jaromail

a commandline tool to easily and privately handle your e-mail
git clone git://parazyd.org/jaromail.git
Log | Files | Refs | Submodules | README

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 }