-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathsend_email
executable file
·651 lines (577 loc) · 19.4 KB
/
send_email
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
#!/bin/bash
# send_email.sh (or SNDEM for short)
# Copyright (c) 2020 Konstantin Gizdov
## Script to send an email with minimal requirements
# Depends only on:
# - coreutils (`base64`, `numfmt`, `stat`)
# - bash
# - curl
# Aimed at sending emails from within
# continuous integration runners
# and other feature-limited light
# environments
set -euo pipefail
IFS=$'\n\t'
__SNDEM_VERSION_MAJOR__="0"
__SNDEM_VERSION_MINOR__="4"
__SNDEM_VERSION_PATCH__="7"
__SNDEM_VERSION__="${__SNDEM_VERSION_MAJOR__}.${__SNDEM_VERSION_MINOR__}.${__SNDEM_VERSION_PATCH__}"
__DEBUG_MODE__=0
__DRYRUN_MODE__=0
__VERBOSE_MODE__=0
__BODY_HTML_MODE__=0
__CMTMSG_MODE__=0
__CMTMSG_CMD_MODE__=0
__REQSUBJ_MODE__=0
__SENDER_NAME__=0
__FILESIZE_LIMIT__=52428800
function verbose_echo {
# only print to screen when second argument is 1
[[ ${2} == 1 ]] && echo "${1}"
return 0
}
function err_echo {
# print to stderr
printf '%s\n' "${1}" >&2 # send message to stderr
}
function fail {
# fail with error message
err_echo "${1}"
exit "${2-1}" # return a code specified by $2 or $1
}
function raw_or_base64 {
# check if input is base64 encoded
# echo 0 if raw
# echo 1 if base64 encoded
$(echo "${1}" | base64 -d 2>/dev/null 1>/dev/null)
local __ret=$?
if [[ ${__ret} != 0 ]]; then
echo "0"
else
echo "1"
fi
}
function decode_either {
# decode a base64 string or
# echo it back if not encoded
if [[ $(raw_or_base64 "${1}") == 0 ]]; then
echo "${1}"
else
echo "${1}" | base64 -d
fi
}
function store_email_creds {
# put email credentials in a file
# for easier reading when passing
# to cURL
echo "user=\"${2}\"" > "${1}"
}
function check_str {
# check if string is defined and not null
# $1 - string to check
# $2 - error message if check fails
if [[ ! -n "${1}" ]]; then
fail "${2}" 1
fi
}
function check_str_opt {
# check if string is defined and not null if
# and only if an option is enabled
# $1 - option to check
# $2 - string to check
# $3 - error message if check fails
if [[ ${1} != 0 ]]; then
check_str "${2}" "${3}"
fi
}
function check_srv {
check_str "${srv_url}" "Email server not specified. Exiting..."
}
function check_subj {
check_str_opt ${__REQSUBJ_MODE__} "${subj_field}" "Email subject not specified. Exiting..."
}
function check_addr_from {
check_str "${addr_from}" "Sender address not specified. Exiting..."
}
function check_name_from {
check_str_opt "${__SENDER_NAME__}" "${name_from}" "Sender name not specified. Exiting..."
}
function check_email_creds {
check_str "${email_creds}" "Email server credentials not specified. Exiting..."
}
function check_addr_to {
check_str "${addr_to_arr}" "Recipient address not specified. Exiting..."
}
function check_name_arr {
# checks whether a variable when read
# as an array will be an empty one
# or not
# returns:
# - 0 if empty
# - 1 if not
IFS=',' read -ra __name_arr <<<"${1},"
if [[ ${#__name_arr[@]} == 1 ]] && [[ ! -n "${__name_arr[0]}" ]]; then
echo 0
else
echo 1
fi
}
function check_size_arrs {
# compares the sizes of two comma
# separated lists (array)
# used to match names to addresses
local __check="$(check_name_arr "${1}")"
if [[ ${__check} == 0 ]]; then
return 0
fi
# assume arrays are stored as comma separated lists
IFS=',' read -ra __name_arr <<<"${1},"
IFS=',' read -ra __addr_arr <<<"${2},"
# get length of $addr_cc_arr and $name_cc_arr arrays
# and compare their sizes
local __name_len=${#__name_arr[@]}
local __addr_len=${#__addr_arr[@]}
if [[ ${__addr_len} != ${__name_len} ]]; then
err_echo "recipients, CC or BCC arrays specifying names and addresses have mismatched sizes."
fail "If you are using the -c/-C or -e/-E flags, you have to provide an equal number of matching pairs. Exiting..." 1
fi
}
function check_file {
# check size of email to be sent
# according to file size limits
if [ ! -e "${1}" ]; then
fail "The file ${1} does not exist." 1
fi
size=$(stat -c%s "${1}")
if (( size > __FILESIZE_LIMIT__ )); then
local __msg="File ${1} is breaking the file size limit of (currently 50MB)."
if [[ ${__DEBUG_MODE__} != 0 || ${__DRYRUN_MODE__} != 0 ]]; then
echo "${__msg}"
return 0
fi
fail "${__msg}" 1
fi
}
function check_env_commit_msg {
if [[ ${__CMTMSG_MODE__} == 0 ]]; then
return 0
fi
local __env_commit_msg="$(decode_either "${SNDEM_CI_COMMIT_MESSAGE}")"
if [[ ! -n ${__env_commit_msg} ]]; then
__env_commit_msg="${CI_COMMIT_MESSAGE}"
fi
check_str_opt ${__CMTMSG_MODE__} "${__env_commit_msg}" "Cannot get commit message from neither environment variable (SNDEM_CI_COMMIT_MESSAGE or CI_COMMIT_MESSAGE): it's empty. Exiting..."
}
function make_env_commit_msg {
if [[ ${__CMTMSG_MODE__} != 1 ]]; then
return 0
fi
local __env_commit_msg="$(decode_either "${SNDEM_CI_COMMIT_MESSAGE}")"
if [[ ! -n ${__env_commit_msg} ]]; then
__env_commit_msg="$(decode_either "${CI_COMMIT_MESSAGE}")"
fi
echo "${__env_commit_msg}"
}
function make_generic_addr_field {
local __name_from="${1}"
local __addr_from="${2}"
local __addr_field=''
if [[ ! -n "${__name_from}" ]]; then
__addr_field="${__addr_from}"
else
__addr_field="\"${__name_from}\" <${__addr_from}>"
fi
echo "${__addr_field}"
}
function make_rpl_to {
local __rpl_to="$(make_generic_addr_field "${name_from}" "${addr_from}")"
echo "${__rpl_to}"
}
function make_from_field {
local __from_field="$(make_generic_addr_field "${name_from}" "${addr_from}")"
echo "${__from_field}"
}
function make_generic_addr_string {
local __addr_string=''
# assume arrays are stored as comma separated lists
IFS=',' read -ra __name_arr <<<"${1},"
IFS=',' read -ra __addr_arr <<<"${2},"
# get length of $addr_arr to iterate
local __addr_len=${#__addr_arr[@]}
for (( i=0; i<${__addr_len}; i++ )); do
set +u
__addr_string+="$(make_generic_addr_field "${__name_arr[$i]}" "${__addr_arr[$i]}")"
set -u
if [[ $i != $((${__addr_len} - 1)) ]]; then
__addr_string+=', '
fi
done
echo "${__addr_string}"
}
function make_to_string {
local __to_field="$(make_generic_addr_string "${name_to_arr}" "${addr_to_arr}")"
echo "${__to_field}"
}
function make_cc_string {
local __cc_string="$(make_generic_addr_string "${name_cc_arr}" "${addr_cc_arr}" )"
echo "${__cc_string}"
}
function make_bcc_string {
local __bcc_string="$(make_generic_addr_string "${name_bcc_arr}" "${addr_bcc_arr}")"
echo "${__bcc_string}"
}
function make_mail_rcpts {
local __mail_rcpts_str=''
# assume arrays are stored as comma separated lists
IFS=',' read -ra __addr_to_arr <<<"${1},"
IFS=',' read -ra __addr_cc_arr <<<"${2},"
IFS=',' read -ra __addr_bcc_arr <<<"${3},"
for i in ${__addr_to_arr[@]}; do
__mail_rcpts_str+="--mail-rcpt ${i} "
done
for i in ${__addr_cc_arr[@]}; do
__mail_rcpts_str+="--mail-rcpt ${i} "
done
for i in ${__addr_bcc_arr[@]}; do
__mail_rcpts_str+="--mail-rcpt ${i} "
done
echo ${__mail_rcpts_str}
}
function make_body_string {
local __body_str="${1}"
local __commit_str="${2}"
if [[ ${__CMTMSG_MODE__} == 1 ]]; then
if [[ "${__body_str}" == *"__CMTMSG_POS__"* ]]; then
__body_str="$(echo "${__body_str/__CMTMSG_POS__/$__commit_str}")"
else
__body_str+="
P.S. Commit message was:
${__commit_str}
"
fi
else
__body_str="${__body_str}"
fi
echo "${__body_str}"
}
function make_body {
echo -e "${body_str}" | base64
}
function make_body_cnt_type {
local __body_html="${1}"
local __body_cnt_type='text/plain'
if [[ ${__body_html} == 1 ]]; then
__body_cnt_type='text/html'
fi
echo "${__body_cnt_type}"
}
function construct_email {
local __file_upload="${1}"
local __body_type="$(make_body_cnt_type "${2}")"
local __start_str="From: ${from_field}
To: ${to_field}
Subject: ${subj_field}
Reply-To: ${rpl_to}
Cc: ${cc_field}
Bcc: ${bcc_field}
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary=\"MULTIPART-MIXED-BOUNDARY\"
--MULTIPART-MIXED-BOUNDARY
Content-Type: multipart/alternative; boundary=\"MULTIPART-ALTERNATIVE-BOUNDARY\"
--MULTIPART-ALTERNATIVE-BOUNDARY
Content-Type: ${__body_type}; charset=utf-8
Content-Transfer-Encoding: base64
Content-Disposition: inline
${body_field}
--MULTIPART-ALTERNATIVE-BOUNDARY--"
echo "${__start_str}" > "${__file_upload}"
local attachment_str=''
local cnt_type='application/octet-stream'
for att in ${attachment_files[@]}; do
attachment_str=''
if [[ ${att##*.} == 'txt' ]]; then
cnt_type='text/plain'
elif [[ ${att##*.} == 'htm' ]]; then
cnt_type='text/html'
elif [[ ${att##*.} == 'html' ]]; then
cnt_type='text/html'
elif [[ ${att##*.} == 'jpg' ]]; then
cnt_type='image/jpeg'
elif [[ ${att##*.} == 'jpeg' ]]; then
cnt_type='image/jpeg'
elif [[ ${att##*.} == 'png' ]]; then
cnt_type='image/png'
elif [[ ${att##*.} == 'pdf' ]]; then
cnt_type='application/pdf'
else
cnt_type='application/octet-stream'
fi
attachment_str+="--MULTIPART-MIXED-BOUNDARY
Content-Type: ${cnt_type}
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=\"${att}\"\n"
echo -e "${attachment_str}" >> "${__file_upload}"
# convert file to base64 and append to the upload file
echo "$(cat "${att}" | base64)" >> "${__file_upload}"
done
# end of uploaded file
echo "--MULTIPART-MIXED-BOUNDARY--" >> "${__file_upload}"
}
function version {
echo "send_email.sh (or SNDEM for short) version ${__SNDEM_VERSION__}"
echo ""
echo "Copyright (c) 2020 Konstantin Gizdov"
}
function help {
# print info & help
version
echo ""
echo "Usage: $0 [options]
note:
By default the values of all options that require input can be
specified either as a raw or base64-encoded string, including
the specific environment variables in use as well. This is very
useful for continuous integration environments (such as GitLab)
which require that masked (secret) variables can only be
base64-encoded. As a side effect a lot of input ambiguities can
be resolved easily. However, keep in mind that each input is
checked for being base64-encoded and if found as such, is
decoded only once. This still allows to pass base64-encoded
strings as variable values if their value is encoded a second
time before being supplied.
options:
-s <server> email server to use
format: <protocol>://example.server.com:<port>
e.g.: smtp://smtp.gmail.com:587
-a <email> email address of sender
-N <string> name of sender
-r <email>[,<email>,...] comma separated list of recipient emails
-R <name>[,<name>,...] comma separated list of recipient names
-c <email>[,<email>,...] comma separated list of CC email addresses
-C <name>[,<name>,...] comma separated list of CC names
corresponding in order to CC email address list
-e <email>[,<email>,...] comma separated list of BCC email addresses
-E <name>[,<name>,...] comma separated list of BCC names
corresponding in order to BCC email address list
-S <string> email subject
-A <file path> file to be attached
must be supplied multiple times for different files
-b <string> email body
-H enable HTML formatted body (plain text by default)
-m <commit message> commit message to be included in email
To put the commit message in a particular
place in your email, supply an email body
with __CMTMSG_POS__ string somewhere inside
it. If not given, commit message will be in
P.S.
-M include environment supplied commit message
in email. Message is taken from either:
- CI_COMMIT_MESSAGE
- SNDEM_CI_COMMIT_MESSAGE
environment variable. When both are
defined, SNDEM_CI_COMMIT_MESSAGE is
preferred. When commit message is
specified on command line, that is used
instead.
-t <username:password> authentication string used to contact email server
-L override file size limit for emails:
can be specified in any format (e.g. 50M or 52428800)
default is 50MB (52428800 bytes)
helper options:
-n dry run - process everything, but don't send an email
implies debug and verbose mode
-D debug mode, nothing will be sent
implies verbose mode
-v verbose mode
-V print version and exit
-h print this help message and exit
environment variables in use:
SNDEM_EMAIL_SERVER specify email server
SNDEM_FROM_EMAIL sender email
SNDEM_FROM_NAME sender name
SNDEM_RCPNT_EMAILS comma separated recipient emails
SNDEM_RCPNT_NAMES comma separated recipient names
SNDEM_CC_EMAILS comma separated CC emails
SNDEM_CC_NAMES comma separated CC names
SNDEM_BCC_EMAILS comma separated BCC emails
SNDEM_BCC_NAMES comma separated BCC names
SNDEM_SUBJECT email subject
SNDEM_EMAIL_AUTH email authorisation string
SNDEM_CI_COMMIT_MESSAGE commit message
CI_COMMIT_MESSAGE commit message
SNDEM_EMAIL_BODY email body
"
}
# trap a temporary file to clean up on exit
file_upload="$(mktemp)"
file_creds="$(mktemp)"
trap "rm -rf ${file_upload} ${file_creds}" EXIT
# read environment
set +u
srv_url="$(decode_either "${SNDEM_EMAIL_SERVER}")"
addr_from="$(decode_either "${SNDEM_FROM_EMAIL}")"
name_from="$(decode_either "${SNDEM_FROM_NAME}")"
addr_to_arr="$(decode_either "${SNDEM_RCPNT_EMAILS}")"
name_to_arr="$(decode_either "${SNDEM_RCPNT_NAMES}")"
addr_cc_arr="$(decode_either "${SNDEM_CC_EMAILS}")"
name_cc_arr="$(decode_either "${SNDEM_CC_NAMES}")"
addr_bcc_arr="$(decode_either "${SNDEM_BCC_EMAILS}")"
name_bcc_arr="$(decode_either "${SNDEM_BCC_NAMES}")"
subj_field="$(decode_either "${SNDEM_SUBJECT}")"
email_creds="$(decode_either "${SNDEM_EMAIL_AUTH}")"
commit_msg="$(decode_either "${SNDEM_CI_COMMIT_MESSAGE}")"
body_str="$(decode_either "${SNDEM_EMAIL_BODY}")"
set -u
attachment_files=()
# get command line option
while getopts "t:c:C:e:E:a:d:N:S:A:b:m:r:R:L:s:HMDvhVn" opt; do
case $opt in
a)
addr_from="$(decode_either "${OPTARG}")"
;;
N)
__SENDER_NAME__=1
name_from="$(decode_either "${OPTARG}")"
;;
r)
addr_to_arr="$(decode_either "${OPTARG}")"
;;
R)
name_to_arr="$(decode_either "${OPTARG}")"
;;
S)
subj_field="$(decode_either "${OPTARG}")"
;;
c)
addr_cc_arr="$(decode_either "${OPTARG}")"
;;
C)
name_cc_arr="$(decode_either "${OPTARG}")"
;;
e)
addr_bcc_arr="$(decode_either "${OPTARG}")"
;;
E)
name_bcc_arr="$(decode_either "${OPTARG}")"
;;
A)
attachment_files+=("$(decode_either "${OPTARG}")")
;;
b)
body_str="$(decode_either "${OPTARG}")"
;;
H)
__BODY_HTML_MODE__=1
;;
m)
__CMTMSG_MODE__=1
__CMTMSG_CMD_MODE__=1
commit_msg="$(decode_either "${OPTARG}")"
;;
M)
__CMTMSG_MODE__=1
;;
s)
srv_url="$(decode_either "${OPTARG}")"
;;
L)
__FILESIZE_LIMIT__="$(numfmt --to=none --from=auto "$(decode_either "${OPTARG}")")"
;;
D)
__DEBUG_MODE__=1
;;
n)
__DRYRUN_MODE__=1
__DEBUG_MODE__=1
__VERBOSE_MODE__=1
;;
v)
__VERBOSE_MODE__=1
;;
V)
version
exit 0
;;
t)
email_creds="$(decode_either "${OPTARG}")"
;;
h)
help
exit 0
;;
:)
help
fail "Option -${OPTARG} needs an argument." 1
;;
\?)
help
fail "Invalid option -- '${OPTARG:-${!OPTIND:-${opt:-}}}'" 1
;;
esac
done
# validation of parameters
check_srv
check_email_creds
check_addr_from
check_name_from
check_addr_to
check_subj
check_size_arrs "${name_to_arr}" "${addr_to_arr}"
check_size_arrs "${name_cc_arr}" "${addr_cc_arr}"
check_size_arrs "${name_bcc_arr}" "${addr_bcc_arr}"
check_env_commit_msg
# put email creds in a file
store_email_creds "${file_creds}" "${email_creds}"
# create needed variables
if [[ ${__CMTMSG_CMD_MODE__} != 1 ]]; then
commit_msg="$(make_env_commit_msg)"
fi
from_field="$(make_from_field)"
rpl_to="$(make_rpl_to)"
to_field="$(make_to_string)"
cc_field="$(make_cc_string)"
bcc_field="$(make_bcc_string)"
body_str="$(make_body_string "${body_str}" "${commit_msg}")"
body_field="$(make_body)"
mail_rcpts_comm="$(make_mail_rcpts "${addr_to_arr}" "${addr_cc_arr}" "${addr_bcc_arr}")"
# write email in a file to be sent
construct_email "${file_upload}" "${__BODY_HTML_MODE__}"
# make sure we're not sending a huge file
check_file "${file_upload}"
curl_command='curl'
if [[ ${__VERBOSE_MODE__} == 1 ]]; then
curl_command+=' -v'
fi
curl_command+=" --url ${srv_url} --ssl-reqd --mail-from ${addr_from} ${mail_rcpts_comm} -T ${file_upload} -K ${file_creds}"
# print debug info and save local copy of email
if [[ ${__DEBUG_MODE__} != 0 ]]; then
cp "${file_upload}" debug.eml
cp "${file_creds}" creds.txt
echo "addr_to_arr: ${addr_to_arr}"
echo "name_to_arr: ${name_to_arr}"
echo "addr_cc_arr: ${addr_cc_arr}"
echo "addr_bcc_arr: ${addr_bcc_arr}"
echo "from_field: ${from_field}"
echo "to_field: ${to_field}"
echo "cc_field: ${cc_field}"
echo "bcc_field: ${bcc_field}"
echo "body_str: ${body_str}"
echo "body_field: ${body_field}"
echo "command that was going to run is:"
echo "${curl_command}"
echo "email file is saved in debug.eml"
echo "credentials file is saved in creds.txt"
fi
# make sure not to send stuff when in dry run mode
if [[ ${__DRYRUN_MODE__} != 0 ]]; then
exit 0
fi
# send email
echo "sending ..."
eval "${curl_command}"
ret=$?
if [[ ${ret} != 0 ]]; then
fail "sending failed with: ${res}" 1
else
fail "OK" 0
fi