commit 255f6ed7c0bb74a939aa78ccbc072efc50769e8d
Author: Jaromil <jaromil@dyne.org>
Date: Wed, 28 Sep 2011 19:20:58 +0200
initial commit
whitepaper and skeleton script
Diffstat:
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 $@