email (16195B)
1 #!/usr/bin/env zsh 2 # 3 # Jaro Mail, your humble and faithful electronic postman 4 # 5 # a tool to easily and privately handle your e-mail communication 6 # 7 # Copyleft (C) 2010-2015 Denis Roio <jaromil@dyne.org> 8 # 9 # This source code is free software; you can redistribute it and/or 10 # modify it under the terms of the GNU Public License as published by 11 # the Free Software Foundation; either version 3 of the License, or 12 # (at your option) any later version. 13 # 14 # This source code is distributed in the hope that it will be useful, 15 # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 17 # Please refer to the GNU Public License for more details. 18 # 19 # You should have received a copy of the GNU Public License along with 20 # this source code; if not, write to: 21 # Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 22 23 # extract all emails found in stdin, one per line 24 stdin_compose() { 25 fn stdin_compose $* 26 27 # among the args parse recipient emails and files to attach 28 _files=() 29 _addrs=() 30 for p in $*; do 31 func "$p" 32 if [[ -r $p ]]; then 33 func "attach: $p" 34 _files+=(-a $p) 35 elif isemail $p; then 36 func "recipient: $p" 37 _addrs+=($p) 38 else 39 warning "skipping arg: $p" 40 fi 41 done 42 43 # take as subject the first line of body 44 ztmp; body=$ztmpfile 45 if command -v rlwrap >/dev/null; then 46 rlwrap -c --multi-line -D2 -b '<>' -I -e '' cat > $body 47 else 48 cat > $body 49 fi 50 subject=`head -n1 $body` 51 52 [[ "$subject" = "" ]] && { 53 error "Nothing read from input, not even the subject" 54 return 1 } 55 notice "Sending mail from commandline" 56 act "recipients: ${_addrs}" 57 [[ "$_files" = "" ]] || act "attachments: ${_files}" 58 act "subject: $subject" 59 read_account ${account} 60 cat <<EOF | x_mutt -s "${subject// /_}" -i $ztmpfile ${=_files} -- ${=_addrs} 61 EOF 62 # _mutt -H <(print "To: ${option_params}") 63 return $? 64 } 65 66 67 stdin_queue() { 68 fn stdin_queue $* 69 70 local base; 71 72 notice "Adding mail to the outbox queue" 73 base="`hostname`-queue-`date +%Y-%m-%d-%H.%M.%S`" 74 75 queue_to=($*) 76 # set it ready for saving in outbux 77 queue_body="$base" 78 79 ztmp 80 tmpqueue=$ztmpfile 81 82 # pre-processing of the email headers 83 awk ' 84 /User-Agent:/ { print "User-Agent: Jaro Mail <https://www.dyne.org/software/jaromail>"; next } 85 { print $0 } 86 ' > "$tmpqueue" 87 [[ $? = 0 ]] || { error "Error queing email from stdin into outbox"; return 1 } 88 89 # calculate the sha1sum of the body to check integrity of delivery 90 _sha1sum=`body $ztmpfile | sha1sum -t` 91 _sha1sum=${_sha1sum[(w)1]} 92 func "Body SHA1 checksum: $_sha1sum" 93 94 maildirmake "$MAILDIRS/outbox" 95 # ${=mkdir} "$MAILDIRS/outbox/send" 96 lock "$MAILDIRS/outbox" 97 98 # check if recipients are a Group 99 if [[ "${=queue_to}" =~ '@jaromail.group' ]]; then 100 101 groupfile="`print ${=queue_to}|cut -d@ -f1`" 102 act "email recipients are in group ${groupfile}" 103 104 [[ -r "$MAILDIRS/Groups/$groupfile" ]] || { 105 maildirmake "$MAILDIRS/postponed" 106 mv "$tmpqueue" "$MAILDIRS/postponed/new" 107 unlock "$MAILDIRS/outbox" 108 error "Group not found: $groupfile" 109 return 1 } 110 111 recipients=`grep -v '^#' $MAILDIRS/Groups/$groupfile` 112 groupnum=`grep -v '^#' $MAILDIRS/Groups/$groupfile | wc -l` 113 groupmode=`head -n1 "$MAILDIRS/Groups/$groupfile" | awk '/^#mode/ { print $2 } { next }'` 114 [[ "$groupmode" = "" ]] && { groupmode="individual" } 115 act "$groupnum recipients in total, sending mode $groupmode" 116 117 case $groupmode in 118 119 # individual group mode hides other recipients and send 120 # multiple mail envelopes with each single recipient in To: 121 individual) 122 for i in ${(f)recipients}; do 123 ig=${base}-${RANDOM} 124 cat "$tmpqueue" | \ 125 awk '/^To:/ { print "'"To: $i"'"; next } { print $0 }' \ 126 | deliver outbox # > "${MAILDIRS}/outbox/new/${ig}.mail" 127 done 128 ;; 129 130 # carboncopy group mode sends a single envelope where all 131 # recipients can see and reply to each other 132 carboncopy|cc) 133 cc="" 134 for i in ${(f)recipients}; do 135 if [ "$cc" = "" ]; then cc="$i" 136 else cc+=", $i"; fi 137 done 138 ig=${base}-${RANDOM} 139 cat "$tmpqueue" | \ 140 awk '/^To:/ { print "'"To: $cc"'"; print "'"Reply-To: $cc"'"; next } 141 { print $0 }' \ 142 | deliver outbox # "${MAILDIRS}/outbox/new/${ig}.mail" 143 ;; 144 esac 145 146 else 147 # recipients are set in the email envelope 148 cat "$tmpqueue" | deliver outbox 149 fi 150 151 unlock "$MAILDIRS/outbox" 152 153 _sha1sum_delivered=`body $last_deliver | sha1sum -t` 154 _sha1sum_delivered=${_sha1sum_delivered[(w)1]} 155 func "Delivered body SHA1 checksum: $_sha1sum_delivered" 156 if [[ "$_sha1sum_delivered" = "$_sha1sum" ]]; then 157 func "correct delivery, SHA1 checksum on body match" 158 return 0 159 fi 160 161 error "Error on delivery, file checksum don't match" 162 [[ $DEBUG = 1 ]] && { 163 func "Differences:" 164 diff $tmpqueue $last_deliver 165 func "-----" 166 } 167 rm -f $last_deliver 168 return 1 169 170 } 171 172 ########### 173 # FETCHMAIL 174 fetchall() { 175 fn fetchall 176 177 notice "Fetching all accounts in $MAILDIRS" 178 res=0 179 accts=`${=find} $MAILDIRS/Accounts -type f | grep -v README` 180 notice "Fetching mail for all accounts: `print ${accts} | wc -l` found" 181 for i in ${(f)accts}; do 182 account=`basename $i` 183 fetch 184 if [ $? != 0 ]; then res=1; fi 185 # returns an error if just one of the accounts did 186 done 187 return $res 188 } 189 190 fetch() { 191 fn fetch $* 192 193 [[ "$account" = "" ]] && { 194 fetchall; return $? } 195 196 # setup global account variables 197 read_account ${account} 198 # name login host protocol port auth folders accountopt 199 [[ $? = 0 ]] || { 200 error "Invalid account: $account" 201 return 1 202 } 203 204 # updates the notmuch configuration 205 nm_setup unread 206 # setup :unread: as default tag 207 208 notice "Fetching email for account ${account}" 209 210 is_online ${imap} ${imap_port} 211 [[ $? = 0 ]] || { return 1 } 212 213 type=imap 214 host=$imap 215 port=$imap_port 216 [[ "$password" = "" ]] && { 217 ask_password 218 [[ $? = 0 ]] || { 219 error "Impossible to fetch email for account ${account}"; 220 return 1 } 221 } 222 223 # this puts total size in $imap_info 224 # experimental only, commented out for now 225 # get_imap_info 226 227 # return here if the imap folders are all empty 228 # { test ${imap_info[${#imap_info}]} = 0 } && { 229 # act "Mailbox is empty, nothing to fetch." 230 # return 0 } 231 232 # notice "Total occupation is `human_size ${imap_info[${#imap_info}]}`" 233 234 fmconf=("poll $imap with proto IMAP user \"$login\" there with password \"$password\"") 235 236 [[ -z $accountopt ]] || { # add option configuration 237 fmconf+=(" ${accountopt} ") } 238 239 # check if folders on commandline 240 if [[ "$1" != "" ]]; then 241 242 unset password 243 244 act "commanded to fetch folders: $1" 245 folders=($1) 246 247 248 else 249 250 # if no folders specified, use all 251 [[ "$folders" == "" ]] && { 252 folders=(`imap_list_folders`) } 253 act "${#folders} folders found" 254 255 # unset here because listing folders still needed a pass 256 unset password 257 258 # nothing to download, bail out with error 259 [[ ${#folders} == "0" ]] && return 1 260 261 # remove excludes 262 [[ ${#exclude} == "0" ]] || { 263 func "exclude folders: $exclude" 264 for e in ${exclude}; do 265 # fuzzy match 266 for f in ${folders}; do 267 [[ "$f" =~ "$e" ]] && { 268 folders=(${folders:#$f}) 269 } 270 done 271 done 272 } 273 274 fi 275 276 if [ "$cert" = "check" ]; then 277 # we now use system-wide certs 278 fmconf+=(" sslcertck ") # sslcertpath '$WORKDIR/certs' 279 fi 280 281 fmconf+=(" sslproto ${transport} warnings 3600 and wants mda \"jaro -q deliver\" ") 282 283 284 fmconf+=(" antispam 571 550 501 554 ") 285 286 [[ $accountopt =~ 'keep' ]] || { 287 warning "planning to delete mails from server, account option: $accountopt" } 288 289 func "fetch folders: $folders" 290 291 # add folder configuration 292 fmconf+=(" folder ${=folders} "); 293 294 # try login without doing anything 295 flog=("${(f)$(print "$fmconf" | fetchmail -v -c -f -)}") 296 297 res=$? 298 # examine result 299 case $res in 300 1) 301 notice "No mails for $name" 302 unset fmconf 303 return 1 304 ;; 305 2) 306 error "Invalid or unknown certificate for $imap" 307 unset fmconf 308 return 1 309 ;; 310 3) 311 error "Invalid password for user $login at $imap" 312 unset fmconf 313 return 1 314 ;; 315 7) warning "Mailbox selection failed (${flog[${#flog}]#*\(}" 316 ;; 317 *) 318 func "fetchmail returns $res" ;; 319 esac 320 321 if [[ $DRYRUN = 0 ]]; then 322 323 act "please wait while downloading mails to incoming..." 324 325 print " $fmconf " | fetchmail -f - | awk ' 326 /^fetchmail: No mail/ { next } 327 /^reading message/ { printf("."); next } 328 { printf("\n%s\n",$0) } 329 END { printf("\n") }' 330 331 else 332 act "dryrun: nothing will be fetched really." 333 fi 334 335 unset fmconf 336 337 return 0 338 } 339 340 ################################################ 341 # read an email from stdin and send it via msmtp 342 smtp_send() { 343 fn smtp-send 344 req=(account) 345 ckreq || return 1 346 347 read_account ${account} 348 [[ $? = 0 ]] || { 349 error "Invalid account: $account" 350 return 1 351 } 352 353 # defaults 354 [[ -z $auth ]] && { auth=plain } 355 [[ -z $smtp_port ]] && { smtp_port=25 } 356 357 notice "SMTP send via account ${account}" 358 359 type=smtp 360 host=$smtp 361 port=$smtp_port 362 363 # load known fingerprints 364 typeset -A smtp_fingerprints 365 [[ -s $MAILDIRS/smtp_fingerprints.zkv ]] && { 366 zkv.load $MAILDIRS/smtp_fingerprints.zkv } 367 known_fingerprint=${smtp_fingerprints[$smtp:$port]} 368 # get the server's fingerprint 369 print QUIT \ 370 | openssl s_client -starttls smtp \ 371 -connect $smtp:$smtp_port \ 372 -showcerts 2>/dev/null \ 373 | openssl x509 -fingerprint -md5 -noout \ 374 | awk -F '=' '/Fingerprint/ {print $2}' | sysread fingerprint 375 fingerprint=$(print $fingerprint | trim) 376 # force printing fingerprint to stderr 377 oldquiet=$QUIET 378 QUIET=0 379 act "known fingerprint: $known_fingerprint" 380 act "server fingerprint: $fingerprint" 381 [[ "$known_fingerprint" = "$fingerprint" ]] || { 382 warning "fingerprint difference detected" 383 # not the same? 384 if [[ "$known_fingerprint" = "" ]]; then 385 # never knew before, save it 386 act "$smtp:$port new fingerprint acknowledged" 387 smtp_fingerprints[$smtp:$port]="$fingerprint" 388 else 389 error "Server fingerprint mismatch!" 390 warning "The known one was different, this may be a man in the middle!" 391 warning "To override and forget the old one, edit $MAILDIRS/smtp_fingerprints.zkv" 392 return 1 393 fi 394 } 395 QUIET=$oldquiet 396 397 zkv.save smtp_fingerprints $MAILDIRS/smtp_fingerprints.zkv 398 399 [[ "$password" = "" ]] && { 400 ask_password 401 [[ $? = 0 ]] || { 402 error "Error retrieving smtp password for $login on $host" 403 unset password 404 return 1 } 405 } 406 407 ztmp 408 msmtpcfg=$ztmpfile 409 sysread -o 1 <<EOF > $msmtpcfg 410 account default 411 from ${email} 412 user ${login} 413 host ${smtp} 414 port ${smtp_port} 415 tls on 416 tls_starttls on 417 tls_certcheck off 418 logfile "${MAILDIRS}/logs/msmtp.log" 419 auth ${auth} 420 password ${password} 421 EOF 422 unset password 423 msmtp -C $msmtpcfg -t 424 res=$? 425 func "msmtp returns: $res" 426 return $res 427 } 428 429 430 ###### 431 # SEND 432 # this function should send all mails in outbox 433 send() { 434 fn send $* 435 436 # list mails to send 437 queue_outbox=`${=find} "${MAILDIRS}/outbox" -type f` 438 queue_outbox_num=`print $queue_outbox | wc -l` 439 [[ "$queue_outbox" = "" ]] && { 440 notice "Outbox is empty, no mails to send." 441 return 0 } 442 443 [[ $DRYRUN = 1 ]] && { return 0 } 444 445 # from here on we must unlock on error 446 lock "${MAILDIRS}/outbox" 447 448 for qbody in ${(f)queue_outbox}; do 449 func "processing outbox queue: $qbody" 450 # clean interrupt 451 [[ $global_quit = 1 ]] && { 452 error "User break requested, interrupting operation" 453 break 454 } 455 456 # check if this is an anonymous mail 457 hdr "$qbody" | grep -i '^from: anon' > /dev/null 458 if [[ $? = 0 ]]; then 459 460 ztmp 461 anoncfg=$ztmpfile 462 463 sysread -o 1 <<EOF > "$anoncfg" 464 REMAIL n 465 POOLSIZE 0 466 SENDPOOLTIME 0m 467 RATE 100 468 469 PGPREMPUBASC /var/lib/mixmaster/used-stats/pubring.asc 470 PUBRING /var/lib/mixmaster/used-stats/pubring.mix 471 TYPE1LIST /var/lib/mixmaster/used-stats/rlist.txt 472 TYPE2REL /var/lib/mixmaster/used-stats/mlist.txt 473 TYPE2LIST /var/lib/mixmaster/used-stats/type2.list 474 475 SENDMAIL=jaro -q smtp 476 ERRLOG=${MAILDIRS}/logs/mixmaster.log 477 VERBOSE=2 478 479 EOF 480 481 notice "Sending out anonymous email via mixmaster" 482 e_addr=() 483 hdr $qbody | e_parse To 484 hdr $qbody | e_parse Cc 485 # cycle through recipients 486 for _e in ${(k)e_addr}; do 487 _n="${(v)e_addr[$_e]}" 488 act "Sending to: $_n <$_e>" 489 490 # parse subject line 491 anonsubj=`hdr "$qbody" | awk ' 492 /^Subject: / { for(i=2;i<=NF;i++) printf "%s ", $i }'` 493 act "Subject: $anonsubj" 494 495 # strip headers and send via mixmaster 496 awk ' 497 BEGIN { body=0 } 498 /^$/ { body=1 } 499 { if(body==1) print $0 } 500 ' "$qbody" \ 501 | mixmaster --config="$anoncfg" -m --to="$_e" --subject="$anonsubj" 502 res=$? 503 func "mixmaster returns $res" 504 done 505 506 else # normal send with msmtp 507 508 act "Sending out email via ${account:-default} account" 509 hdr "$qbody" | awk ' 510 /^From:/ { print " . " $0 } 511 /^To:/ { print " . " $0 } 512 /^Cc:/ { print " . " $0 } 513 /^Subject:/ { print " . " $0 } 514 ' 515 516 tsize=`zstat +size "$qbody"` 517 act "sending `human_size $tsize` over the network ..." 518 jaro -q smtp -a ${account:-default} < "${qbody}" 519 res=$? 520 fi 521 522 # evaluate results 523 if [[ "$res" != "0" ]]; then 524 error "Error sending mail, skipped" 525 else 526 notice "Mail sent succesfully" 527 # whitelist those to whom we send mails 528 hdr "$qbody" | learn recipient 529 printfile "$qbody" | deliver sent 530 [[ $? = 0 ]] && { rm "$qbody" } 531 fi 532 533 done 534 535 unlock "$MAILDIRS/outbox" 536 537 return $res 538 } 539 540 ###### 541 # PEEK 542 # this function will open the MTA to the imap server without fetching mails locally 543 peek() { 544 fn peek $* 545 546 read_account ${account} 547 [[ $? = 0 ]] || { 548 error "Invalid account: $account" 549 return 1 550 } 551 552 is_online ${imap} ${imap_port} 553 { test $? = 0 } || { return 1 } 554 555 notice "Peek into remote imap account $name" 556 557 folder="" 558 if ! [ -z ${1} ]; then 559 folder="${1}" 560 act "opening folder ${folder}" 561 fi 562 563 case $transport in 564 SSL*|TLS*) act "using secure connection ($transport)" 565 iproto="imaps" ;; 566 plain) act "using clear text connection" 567 iproto="imap" ;; 568 *) 569 error "Unknown transport: $transport" 570 error "Configuration error in imap account ${account}" 571 return 1 ;; 572 esac 573 # escape at sign in login 574 ilogin=`print $login | sed 's/@/\\@/'` 575 576 { test $DRYRUN != 1 } && { 577 578 type=imap 579 host=$imap 580 port=$imap_port 581 582 x_mutt -f ${iproto}://${ilogin}@${imap}:${imap_port}/${folder} 583 584 } # DRYRUN 585 return $? 586 }