jaromail

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

commit 255f6ed7c0bb74a939aa78ccbc072efc50769e8d
Author: Jaromil <jaromil@dyne.org>
Date:   Wed, 28 Sep 2011 19:20:58 +0200

initial commit

whitepaper and skeleton script

Diffstat:
Adoc/postino-diagram.dia | 0
Adoc/postino-whitepaper.org | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/postino | 371+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 488 insertions(+), 0 deletions(-)

diff --git a/doc/postino-diagram.dia b/doc/postino-diagram.dia Binary files differ. diff --git a/doc/postino-whitepaper.org b/doc/postino-whitepaper.org @@ -0,0 +1,117 @@ +#+TITLE: Postino Suite - whitepaper +#+AUTHOR: Jaromil +#+DATE: September 2010 - 2011 + +#+LaTeX_CLASS: article +#+LaTeX_CLASS_OPTIONS: [a4,onecolumn,portrait] +#+LATEX_HEADER: \usepackage[utf8x]{inputenc} +#+LATEX_HEADER: \usepackage[T1]{fontenc} +#+LATEX_HEADER: \usepackage{hyperref} +#+LATEX_HEADER: \usepackage[pdftex]{graphicx} +#+LATEX_HEADER: \usepackage{fullpage} +#+LATEX_HEADER: \usepackage{lmodern} +#+LATEX_HEADER: \usepackage[hang,small]{caption} +#+LATEX_HEADER: \usepackage{float} + + +* Introduction + +Postino is an integrated suite of interoperable tools to manage e-mail +communication in a private and efficient way, without relying on third +party services. Rather than reinventing the wheel, this suite reuses +existing free and open source tools and protocols and is mainly +targeted for GNU/Linux/BSD desktop usage. + +* Diagram + + +#+LATEX_BEGIN +\begin{figure}[htb!] + \caption{Suite diagram} + \centering + \includegraphics{postino-diagram.png} +\end{figure} +#+LATEX_END + + +* Components + +** Client side + ++ Mail User Agent[fn:mua] ++ Fetchmail ++ Procmail & Procmail-lib ++ Mini SMTP (msmtp) ++ Little Brother DB ++ Secure shell client + +[fn:mua] Can be any application supporting local maildir folders, our favourite is Mutt + +** Server side + ++ Postfix ++ Dovecot ++ Sieve ++ Secure shell server + +* Workflow + +** Configuration + +*** Configure to receive mail + +*** Configure to send mail + +*** Configure to filter mail + +#+BEGIN_EXAMPLE +unset POSTRULE +POSTRULE+="from: list@kernel.org save: ml.kernel %%" +POSTRULE+="to: jaromil@nimk.nl save: job.nimk %%" +#+END_EXAMPLE + + + +*** Example configuration + +**** main.conf + +#+BEGIN_EXAMPLE +# ACCEPTED EMAIL ADDRESSES +EMAIL="jaromil.rojo@gmail.com, jaromil@dyne.org, jaromil@kyuzz.org" + +# LOCAL FILES +MAIL_USER_AGENT=mutt +MAIL_TRANSPORT_AGENT=msmtp +MAILDIRS=$HOME/mail +WORKDIR=$HOME/.postino +CERTIFICATES=$HOME/.ssl/certs +REMOTE_FILTER=/var/mail/... +#+END_EXAMPLE + +**** send.conf + +#+BEGIN_EXAMPLE +# name host port login +gmail smtp.gmail.com 25 jaromil.rojo@gmail.com +dyne assata.dyne.org 25 username@dyne.im +#+END_EXAMPLE + +**** receive.conf +#+BEGIN_EXAMPLE +# name host port login +gmail imap.gmail.com 443 jaromil.rojo@gmail.com +#+END_EXAMPLE + + + +** Operation + +*** Fetch mail + +*** Read and reply mail + +*** Send mail + +*** Sync backup and filters + diff --git a/src/postino b/src/postino @@ -0,0 +1,371 @@ +#!/bin/zsh +# +# Postino, an humble and faithful postman +# +# a tool to easily and privately handle your e-mail communication +# +# Copyleft (C) 2010-2011 Denis Roio <jaromil@dyne.org> +# +# This source code is free software; you can redistribute it and/or +# modify it under the terms of the GNU Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This source code is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# Please refer to the GNU Public License for more details. +# +# You should have received a copy of the GNU Public License along with +# this source code; if not, write to: +# Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +VERSION=0.5 +DATE=Sept/2011 +POSTINOEXEC=$0 +typeset -a OLDARGS +for arg in ${argv}; do OLDARGS+=($arg); done + +#declare global variables +QUIET=0 +DEBUG=0 +typeset -A global_opts +typeset -A opts + +# parse configuration +source $HOME/.postinorc +mkdir -p $WORKDIR/tmp + +autoload colors; colors + +# standard output message routines +# it's always useful to wrap them, in case we change behaviour later +notice() { if [[ $QUIET == 0 ]]; then print "$fg_bold[green][*]$fg_no_bold[white] $1" >&2; fi } +error() { if [[ $QUIET == 0 ]]; then print "$fg[red][!]$fg[white] $1" >&2; fi } +func() { if [[ $DEBUG == 1 ]]; then print "$fg[blue][D]$fg[white] $1" >&2; fi } +act() { + if [[ $QUIET == 0 ]]; then + if [ "$1" = "-n" ]; then + print -n "$fg_bold[white] . $fg_no_bold[white] $2" >&2; + else + print "$fg_bold[white] . $fg_no_bold[white] $1" >&2; + fi + fi +} + +# we use pinentry +# comes from gpg project and is secure +# it also conveniently uses the right toolkit +ask_password() { + + # pinentry has no custom icon setting + # so we need to temporary modify the gtk theme + if [ -r /usr/local/share/themes/tomb/gtk-2.0-key/gtkrc ]; then + GTK2_RC=/usr/local/share/themes/tomb/gtk-2.0-key/gtkrc + elif [ -r /usr/share/themes/tomb/gtk-2.0-key/gtkrc ]; then + GTK2_RC=/usr/share/themes/tomb/gtk-2.0-key/gtkrc + fi + + cat <<EOF | GTK2_RC_FILES=${GTK2_RC} pinentry 2>/dev/null | awk '/^D / { sub(/^D /, ""); print }' +OPTION ttyname=$TTY +OPTION lc-ctype=$LANG +SETTITLE Insert tomb password +SETDESC Open tomb: $1 +SETPROMPT Password: +GETPIN +EOF + +} + +option_is_set() { + #First argument, the option (something like "-s") + #Second (optional) argument: if it's "out", command will print it out 'set'/'unset' + # This is useful for if conditions + #Return 0 if is set, 1 otherwise + [[ -n ${(k)opts[$1]} ]]; + r=$? + if [[ $2 == out ]]; then + if [[ $r == 0 ]]; then + echo 'set' + else + echo 'unset' + fi + fi + return $r; +} +option_value() { + #First argument, the option (something like "-s") + <<< ${opts[$1]} +} + +###### +# SMTP +send() { + # this function should send all mails in queue + + local -aU smtp_set #behave like a set; that is, an array with unique elements + smtp_set=`awk '!/^#/ { print $1 ";" $2 ";" $3 ";" $4 }' $WORKDIR/send.conf` + for s in ${(f)smtp_set}; do + sname="${s[(ws:;:)1]}" + shost="${s[(ws:;:)2]}" + sport="${s[(ws:;:)3]}" + slogin="${s[(ws:;:)4]}" + func "SMTP: $sname $slogin $shost:$sport" + done + error "TODO" + return 0 +} + +###### +# IMAP +peek() { + # this function will open the MTA to the imap server without fetching mails locally + local -aU imap_set #behave like a set; that is, an array with unique elements + imap_set=`awk '!/^#/ { print $1 ";" $2 ";" $3 ";" $4 }' $WORKDIR/receive.conf` + for i in ${(f)imap_set}; do + iname="${i[(ws:;:)1]}" + # look for selection, not default + if [ $1 ] && [ "$1" != "$iname" ]; then continue; fi + ihost="${i[(ws:;:)2]}" + iport="${i[(ws:;:)3]}" + ilogin="${i[(ws:;:)4]}" + func "IMAP: $iname $ilogin $ihost:$iport" + if ! [ $1 ]; then break; fi # take first as default + done + # escape at sign in login + escilogin=`echo $ilogin | sed 's/@/\\@/'` + print "mutt -f imaps://${escilogin}@${ihost}:$iport" + mutt -f imaps://`echo $ilogin | sed 's/@/\\@/'`@${ihost}:$iport + return 0 +} + +sync() { + # this function should: + # parse all filters + # generate procmailrc + # generate muttrc + # backup what's too old in the maildirs + # ... + + # debug configuration + func "WORKDIR: $WORKDIR" + func "MAILDIRS: $MAILDIRS" + + ###### + # MUTT + mkdir -p $WORKDIR/mutt + touch $WORKDIR/mutt/rc + cat<<EOF >> $WORKDIR/mutt/rc +# mutt config generated by postino +unset use_domain +set hostname = "dyne.org" +set realname = "jaromil" +set folder = ~/$MAILDIRS +set spoolfile = ~/$MAILDIRS/known/ +set record = ~/$MAILDIRS/sent/ +set postponed=~/$MAILDIRS/postponed/ +set tmpdir = $WORKDIR/tmp +set query_command = "postino query '%s'" +set header_cache=~/$WORKDIR/mutt/cache +set maildir_header_cache_verify=no +# mailboxes in order of priority +source $WORKDIR/mutt/mboxes +## end of postino muttrc +EOF + + # just the header, will be completed later in procmail loop + rm -f $WORKDIR/mutt/mboxes + echo -n "mailboxes +priv" > $WORKDIR/mutt/mboxes + + ########## + # PROCMAIL + mkdir -p $WORKDIR/procmail + rm -f $WORKDIR/procmail/rc + touch $WORKDIR/procmail/rc + cat<<EOF >> $WORKDIR/procmail/rc +# procmail configuration file generated by postino +MAILDIR=$MAILDIRS +DEFAULT=unsorted/ +VERBOSE=off +LOGFILE=$WORKDIR/procmail/log +SHELL = /bin/sh # VERY IMPORTANT +UMASK = 007 # James Bond :-) +LINEBUF = 8192 # avoid procmail choke +# Using Procmail Module Library http://sf.net/projects/pm-lib +PMSRC = /usr/share/procmail-lib +# Load the central initial startup code. +INCLUDERC = $PMSRC/pm-javar.rc +PF_DEST = "" # clear these vars +PF_FROM = "" +# don't save multiple copies +PF_RECURSE = yes +:0 +* ? test pf-chkto.rc +{ +# filters generated from postino filters.conf +EOF + for f in `cat $WORKDIR/filters.conf | awk '/^#/ {next} /^./ { print $1 ";" $2 ";" $3 ";" $4 }'`; do + header="${f[(ws:;:)1]}" + address="${f[(ws:;:)2]}" + action="${f[(ws:;:)3]}" + destination="${f[(ws:;:)4]}" + case $header in + to) + print "ADDR=${address}\tDEST=${destination}\tINCLUDERC=$PMSRC/pf-chkto.rc" \ + >> $WORKDIR/procmail/rc + ;; + from) + print "ADDR=${address}\tDEST=${destination}\tINCLUDERC=$PMSRC/pf-check.rc" \ + >> $WORKDIR/procmail/rc + ;; + *) + error "unsupported in filters.conf: $header (skipped)" + ;; + esac + # MUTT (generate mailboxes priority this parser) + echo " \\" >> $WORKDIR/mutt/mboxes + echo -n " +${destination} " >> $WORKDIR/mutt/mboxes + done + + uniq $WORKDIR/mutt/mboxes > $WORKDIR/tmp/mboxes + mv $WORKDIR/tmp/mboxes $WORKDIR/mutt/mboxes + echo " \\" >> $WORKDIR/mutt/mboxes + echo " +unsorted" >> $WORKDIR/mutt/mboxes + + cat <<EOF >> $WORKDIR/procmail/rc +} + +# save the mails +:0 +* PF_DEST ?? . +* ? test $PMSRC/pf-save.rc +{ INCLUDERC=$PMSRC/pf-save.rc } + +# if the sender is known (ldbd recognizes it) then put mail in high priority 'known' +:0 w: +* ? formail -x"From:" | head -n1 | tr 'A-Z' 'a-z' | sed 's/.*\W\([0-9a-z_.-]\+@[0-9a-z_.-]\+\).*/\1/' | xargs lbdbq +known/ + +# if the destination is known, put it in private folder +:0 +* ? test $PMSRC/pf-chkto.rc +{ + ADDR="(jaromil@dyne.org)" DEST=priv/ INCLUDERC=$PMSRC/pf-chkto.rc + ADDR="(jaromil@nimk.nl)" DEST=priv/ INCLUDERC=$PMSRC/pf-chkto.rc + ADDR="(jaromil@kyuzz.org)" DEST=priv/ INCLUDERC=$PMSRC/pf-chkto.rc + ADDR="(jaromil@enemy.org)" DEST=priv/ INCLUDERC=$PMSRC/pf-chkto.rc + ADDR="(j@rastasoft.org)" DEST=priv/ INCLUDERC=$PMSRC/pf-chkto.rc + ADDR="(denis@roio.net)" DEST=priv/ INCLUDERC=$PMSRC/pf-chkto.rc +} + +# if got here, go to unsorted + +# save the mails +:0 +* PF_DEST ?? . +* ? test $PMSRC/pf-save.rc +{ INCLUDERC=$PMSRC/pf-save.rc } + +EOF + return 0 +} + + +main() + { + local -A subcommands_opts + ### Options configuration + #Hi, dear developer! Are you trying to add a new subcommand, or to add some options? + #Well, keep in mind that: + # 1. An option CAN'T have differente meanings/behaviour in different subcommands. + # For example, "-s" means "size" and accept an argument. If you are tempted to add + # an option "-s" (that means, for example "silent", and doesn't accept an argument) + # DON'T DO IT! + # There are two reasons for that: + # I. usability; user expect that "-s" is "size + # II. Option parsing WILL EXPLODE if you do this kind of bad things + # (it will say "option defined more than once, and he's right) + main_opts=(q -quiet=q D -debug=D h -help=h v -version=v n -dry-run=n) + subcommands_opts[__default]="" + subcommands_opts[fetch]="k -keep=k" + subcommands_opts[send]="" + subcommands_opts[peek]="" + subcommands_opts[sync]="" + subcommands_opts[conf]="" +# subcommands_opts[mount]=${subcommands_opts[open]} +# subcommands_opts[create]="s: -size=s -ignore-swap k: -key=k" + ### Detect subcommand + local -aU every_opts #every_opts behave like a set; that is, an array with unique elements + for optspec in $subcommands_opts$main_opts; do + for opt in ${=optspec}; do + every_opts+=${opt} + done + done + local -a oldstar + oldstar=($argv) + zparseopts -M -E -D -Adiscardme ${every_opts} + unset discardme + subcommand=$1 + if [[ -z $subcommand ]]; then + subcommand="__default" + fi + if [[ -z ${(k)subcommands_opts[$subcommand]} ]]; then #there's no such subcommand + error "Subcommand '$subcommand' doesn't exist" + exit 127 + fi + argv=(${oldstar}) + unset oldstar + + ### Parsing global + command-specific options + # zsh magic: ${=string} will split to multiple arguments when spaces occur + set -A cmd_opts ${main_opts} ${=subcommands_opts[$subcommand]} + if [[ -n $cmd_opts ]]; then #if there is no option, we don't need parsing + zparseopts -M -E -D -Aopts ${cmd_opts} + if [[ $? != 0 ]]; then + error "Some error occurred during option processing." + exit 127 + fi + fi + #build PARAM (array of arguments) and check if there are unrecognized options + ok=0 + PARAM=() + for arg in $*; do + if [[ $arg == '--' || $arg == '-' ]]; then + ok=1 + continue #it shouldnt be appended to PARAM + elif [[ $arg[1] == '-' ]]; then + if [[ $ok == 0 ]]; then + error "unrecognized option $arg" + exit 127 + fi + fi + PARAM+=$arg + done + #first parameter actually is the subcommand: delete it and shift + if [[ $subcommand != '__default' ]]; then + PARAM[1]=() + shift + fi + ### End parsing command-specific options + + if option_is_set -v; then act "Postino - $VERSION"; fi + if option_is_set -h; then error "TODO usage here"; fi + if option_is_set -q; then QUIET=1; fi + if option_is_set -D; then func "Debug messages ON"; DEBUG=1; fi + + case "$subcommand" in + fetch) ;; + send) ;; + peek) peek ;; + sync) sync ;; + conf) ;; + __default) ;; + *) error "command \"$subcommand\" not recognized" + act "try -h for help" + return 1 + ;; + esac + return 0 +} + +main $@