Прикладная демонология, практические рецепты

Мой почтовый setup

Не могу не процитировать: "All mail clients suck. This one just sucks less."
Эта фраза стала визитной карточкой программы Mutt. И хотя ей уже больше 20 лет, увы, ее актуальность ничуть не снизилась.
Не буду дискутировать о проблемах различных MUA, расскажу какой выбор сделал я.

Центральным компонентом системы является сервер на котором перманентно работают fetchmail и imapfilter.

Программа fetchmail запущена в нескольких экземплярах, чтобы забирать почту с различных аккаунтов.
С некоторых аккаунтов забирается протоколом IMAP, с других (например GMail) - удобнее использовать POP3.
Вся почта забирается с удалением на сервере.
Забранная почта без какой-либо обработки сбрасывается на входящий SMTP основного хранилища, которое настроено таким образом, чтобы все входящие письма оказывались в IMAP каталоге spool.

Пример конфигурации fetchmail (~/.fetchmailrc):

set daemon 60

skip dovecot via mail.example.com proto imap timeout 300 tracepolls user login1 there is login@example.com here ssl smtphost my.smtphost.com batchlimit 10 fetchall idle
skip google via imap.gmail.com proto pop3 timeout 300 tracepolls user login2 there is login@example.com here ssl smtphost my.smtphost.com batchlimit 10 fetchall

Перезапускается сбор почты таким скриптом (~bin/restart.fetchmail):

#!/bin/sh

mkdir -p ~/var/log ~/var/run

for a in dovecot google; do
    logfile=~/var/log/fetchmail.$a.log
    pidfile=~/var/run/fetchmail.$a.pid
    test -f $pidfile && kill `head -1 $pidfile`
    touch $logfile
    fetchmail --logfile $logfile --pidfile $pidfile $a
done

Программа imapfilter запущена в единственном экземпляре и при помощи IMAP IDLE постоянно мониторит каталог spool на предмет новой входящей почты.
Обнаружив ее - обрабатывает в соответствии с конфигурационным файлом: раскладывает по другим каталогам, маркирует как важное, прочтенное или просто удаляет.

Ее конфигурация (~/.imapfilter/config.lua) пишется на Lua и выглядит примерно так:

options.timeout = 10

imap = IMAP {
    server = 'imap.example.com',
    username = 'login',
}

-- filtering
function loop()
    -- all incoming
    msgs = imap.spool:select_all()

    -- maintenance reports
    res = msgs:contain_subject('daily run output') *
          msgs:contain_from('root@')
    res:move_messages(imap.daily)
    msgs = msgs - res

    res = msgs:contain_subject('weekly run output') *
          msgs:contain_from('root@')
    res:move_messages(imap.weekly)
    msgs = msgs - res

    -- maillists
    res = msgs:contain_field('X-BeenThere','nginx@nginx.org')
    res:move_messages(imap['nginx'])
    msgs = msgs - res

    res = msgs:contain_field('X-BeenThere','nginx-ru@nginx.org')
    res:move_messages(imap['nginx-ru'])
    msgs = msgs - res

    res = msgs:contain_field('X-BeenThere','freebsd-announce@freebsd.org')
    res:move_messages(imap['freebsd-announce'])
    msgs = msgs - res

    res = msgs:contain_field('X-BeenThere','freebsd-security@freebsd.org')
    res:move_messages(imap['freebsd-security'])
    msgs = msgs - res

    -- all the rest
    msgs:move_messages(imap.INBOX)

    -- expunge reports
    res = imap.daily:is_older(30)
    res:delete_messages()

    res = imap.weekly:is_older(365)
    res:delete_messages()

    -- expunge trash
    res = imap.trash:is_older(3)
    res:delete_messages()

    -- waiting...
    imap.spool:enter_idle()
end

-- daemonize
become_daemon(3,loop)

Скрипт перезапуска выглядит совсем просто (~/bin/restart.imapfilter):

#!/bin/sh

pkill -u`whoami` imapfilter
imapfilter

В местах чтения почты (workstations) используется программа isync, в задачу которой входит двухсторонняя синхронизация локальных почтовых каталогов с удаленным IMAP.

Ее конфигурация (~/.mbsyncrc) может выглядеть так:

# global configuration section
CopyArrivalDate yes
Create Both
Expunge Both
SyncState *

# local store
MaildirStore local
Flatten .
Inbox ~/Mail/INBOX
Path  ~/Mail/

# IMAP accounts
IMAPAccount storage
Host my.example.com
User login
PassCmd "security find-generic-password -w -a login -s my.example.com"
SSLType STARTTLS
SSLVersions TLSv1.2
TimeOut 60
CertificateFile ~/.mbsync/my.example.com.crt

# remote store
IMAPStore storage
Account storage

# INBOX only
Channel inbox
Master :storage:
Slave  :local:

# important mailboxes
Channel main
Master :storage:
Slave  :local:
Pattern INBOX foo-% trash

# full set of mailboxes
Channel full
Master :storage::
Slave  :local:
Pattern *
Pattern !Sent !Spam !spool

Для периодического запуска isync в отдельном терминале используется простой скрипт (~/bin/posty):

#!/bin/sh

clear=30  # clear screen every n-th loop
full=20   # do full sync every n-th loop
main=10   # do main folders sync every n-th loop
sleep=60  # sleep after loop complete

if [ $# -eq 1 ]; then
    sleep=$1
fi

left="\e[D"
loop=0
while true; do
    if [ `expr $loop % $clear` -eq 0 ]; then
        clear
    fi
    if [ ` expr $loop % $full` -eq 0 ]; then
        channel=full
        color="0;31"
    elif [ ` expr $loop % $main` -eq 0 ]; then
        channel=main
        color="0;32"
    else
        channel=inbox
        color="0;34"
    fi

    start=`date +%s`
    printf "#%04d \e[${color}m%-7s\e[0m at `date -jr $start +%H:%M:%S` ... " $loop "[$channel]"
    mbsync -q $channel
    finish=`date +%s`
    printf "elapsed %2ds, waiting ${sleep}s: " `expr $finish - $start`
    for i in `jot $sleep`; do
        sleep 1
        if [ $i -gt 1 ]; then
            for j in `jot $(expr $(($i-1)) : '.*')`; do
                printf $left
            done
            printf $left
       fi
        printf "$i "
    done
    printf "$left, awaked!\n"
    (( loop+=1 ))
done

Далее, с локальными каталогами работает непосредственно Mutt в котором для форсированной синхронизации определены следующие макросы:

# sync IMAP
macro index ,sf ': unset wait_key<enter><shell-escape>mbsync full<enter>:  set wait_key<enter>'
macro index ,si ': unset wait_key<enter><shell-escape>mbsync inbox<enter>: set wait_key<enter>'
macro index ,sm ': unset wait_key<enter><shell-escape>mbsync main<enter>:  set wait_key<enter>'

Плюсы решения

Слабые стороны