同步 Claws Mail 的回收站到邮件服务器

Gmail 依然是经常抽风,没办法了,只好弄个客户端用, 至少工作的时候用起来比较方便,不用老是等待、重试。

我的使用模式比较复杂,简单描述如下: 工作邮箱主要从 Web 和 Mutt 客户端访问, 主要作用一个是搜索浏览,一个是存档。 所有邮件还会自动转发到另外一个个人邮箱, 这个邮箱使用 Claws Mail 客户端下载,本地管理。 日常工作中,以个人邮箱的客户端为主, 工作邮箱的 Web 界面为辅。

这篇文章所解决的问题就是, 在 Claws Mail 客户端中删除邮件后, 实现这些邮件在上述两个邮箱中也删除到回收站。 用于处理一些垃圾邮件和确定不再需要的正常邮件。

如果用 IMAP 客户端来管理可以达到同样效果, 但我的邮件比较多, IMAP 太慢了,动不动就要刷新上千封邮件信息, 耗不起。

闲话扯完,下面来讲处理思路和一些心得, 希望直接找结果的请猛击 imap-del-for-mh.php ,运行环境中需要包含 Fwlib

  1. 定位邮件文件

    Claws Mail 使用 MH 格式存储邮件, 一封邮件对应一个文件, 一个目录就对应 Claws Mail 中的一个目录, 结构非常清晰。

  2. 连接邮件服务器

    由于邮件都已经使用 POP3 方式下载过了, 所以这里只能采用 IMAP 方式连接了(不然列不出邮件)。 连接 Gmail 不敢说快,倒也能够接受。

  3. 解析邮件文件,找出用于在服务器上定位邮件的必要信息

    读文件,用正则。 IMAP 中查找邮件所用时间是 d-M-Y ,减号可以省略, 比如 11-May-2013 或者 11 May 2013 。 From 和 Subject 中可能会有非英文字符,需要进行转码。 Message-ID 应该是邮件的唯一标识, 但不是是否因为其包含了部分隐私信息的原因, IMAP 服务器没法依据它来检索邮件。

  4. 第一次查找、比对邮件

    由于 IMAP 并没有提供准确查找、定位邮件的方法, 所以只能按照各种条件来检索邮件, 然后一封封的检查 Message-ID, 看与本地邮件文件中是否一致。

    第一次查找邮件使用 From 发件人和 Date 邮件时间, 尝试过使用 Subject 邮件标题, 反正有可能搜索不到, 原因大概是无法依据其中的中文进行模糊匹配。 From 也是只使用 <> 尖括号以内的部分,更准确。 Date 检索条件稍微麻烦一点,PHP 文档中有 ON date 方法, 但实际没法用,所以使用的是 SINCE [前一天] BEFORE [后一天] 。 由于无论怎么检索都无法匹配, 转而寻找“一定”包含指定邮件的最小结果集。

    这种方式能够匹配到大部分正常发送的邮件。

  5. 第二次查找、比对邮件

    一些不正常邮件,尤其以各类垃圾邮件为代表, 存在着信息不全、时间不对等稀奇古怪的问题, 所以如果第一次没有找到,再换条件补查一次。 这次使用的是邮件头中的 Recieved 接收时间, 这是邮件服务器接收到这封邮件的时间, 然后以这个时间为基准, 在 前一天、后一天 的范围内查找邮件、逐一匹配。 虽然速度要慢一些, 但理论上所有邮件采用这种方式都能找到。

  6. 删除邮件

    通过 Message-ID 匹配准确定位到邮件后, 便得到了邮件的 UID。 UID 和 msgid 是不同的,msgid 是在某一目录或者列表下的序号, 而 UID 才是 IMAP 下邮件的唯一编号。 不过邮件被删除到回收站,再从回收站恢复的时候, UID 也是会改变的。

    Gmail 的 IMAP delete 操作和一般的“删除”含义并不同, 只是把邮件从 Inbox 中去除了,跑到 All Mails 里面了, 所以把邮件先 move 到 trash 目录下,再进行 delete, 最后执行 expunge 操作。

最后回想一下,如果一次性把所有邮件头都读取出来, 再和本地的 Message-ID 进行对应, 在需处理邮件相当多的情况下应该会有总体效率的提升, 但不适用于我这个场景

如果 IMAP 能够直接使用 Message-ID 来精确匹配, 那就省事多了。

-EOT-

一直在忙,聊以凑数。

参考

Apple Mail 邮件太多不下载问题的解决

Apple Mail version3.6(936)

原先拿到这个 mac 本本的时候,由于家中网速比较慢,收发邮件比较折磨人,而我又属于对邮件依赖较重的,所以专门申请了个新的 gmail 帐号,专门用在这个机器上,把邮件下载到本地便于处理(平时个人邮件都是自己发给自己的 N 个邮箱共享),顺便体验下 Apple Mail 软件。

Apple Mail 倒算是中规中矩,按下不表,中间有一段时间没怎么用这台 mac,而这个专用 gmail 帐号里面堆积的邮件应该也不少了,简单估计有几百封吧。终于,等到我想用的那一天,突然发现它不下载新邮件了。

逐项检查,系统和软件工作正常,网络正常,gmail 登录进去肯定是有新邮件,而且 pop 设置正常。所有的东西都没有动过,并且原先刚设置好这个帐号的时候,邮件下载都是正常的。Command+0 打开 Activity 窗口监视──连接、登录、查找新邮件、退出,貌似正常,但完全无视我邮箱中赫然在目的新邮件哪。

纯属无意中翻看 gmail 的在线帮助,其实对这类帮助并不报太大希望的,因为一般上面只有一些常规问题的答案,我的问题比较蹊跷,八成没有。帮助里面专门有针对 Apple Mail 3.0 Troubleshooting,列了几个问题,对照我的情况,逐个回答如下:

  • Have you recently used Gmail’s POP service with this client and address?
  • --> Yes, this address just worked recently
  • When are you encountering problems?
  • --> Downloading
  • Does your client display an error?
  • --> No, my mail client does not show an error
  • Downloading issues
  • --> Some or all messages don’t download

然后帮助给了4个解决方法,其中第2条是亮点:

Try enabling recent mode in your POP client by replacing 'username@gmail.com'
in the Username field of your POP client settings with 'recent:username@gmail.com',
and uncheck the box next to Remove copy from server after retrieving a message
in your Advanced POP client settings. Recent mode fetches the last 30 days of mail,
regardless of any other factors. If you don't Leave messages on server your 
messages will be moved to Trash after download.

看仔细了,在邮件帐户前面加上 recent:,只下载最近30天的邮件,这个以前没听说过,从之,我的邮件又愉快的下载了。至于30天以前的邮件如何下载,我是不需要了。至于为什么会这样,还真不明白是 gmail 这里有限制,还是 Apple Mail 无法读取30天以前邮件的邮件头导致无法下载,希望有明白人指点一二。

附 gmail 帮助截图为据:

Gmail帮助中关于Apple Mail的Troubleshooting

[Git]提交后自动发email

当然是通过hooks来实现了,对应post-receive,脚本也是已经随git-core安装就有了的:/usr/share/doc/git-core/contrib/hooks/post-receive-email,不过这个脚本用到了sendmail,我想好多机器上都没配这个东东吧,好在有好心人在这个脚本的基础上进行了完善,可以用msmtp发信了,看来我以前换用msmtp配mutt太正确了,另外msmtp也确实比sendmail小巧多了。

首先把刚才说到的脚本以及依赖文件functions一起下载到某地,加上执行属性(公用),然后ln到仓库的hooks目录下:

$ cd test.git/hooks/
$ mv post-receive post-receive.origin
$ ln -s ../../gittools/post-receive-email post-receive
$ ln -s ../../gittools/functions

然后修改仓库里的config文件,注意不是客户端的:

[hooks "post-receive-email"]
    mailinglist = list1@domain.tld, list2@domain.tld
    announcelist =
    envelopesender = mailsender@domain.tld
    sendmail = /usr/bin/msmtp

其中:

  • mailinglist 默认的收信人,留空就不发信了
  • announcelist 创建tag时发送从上次创建tag以来的汇总邮件的收信人,比如用版本号作为tag的时候这就生成了每个版本的changelog,留空则用mailinglist的值
  • envelopesender 发信人/账号,要和msmtp的发信账号对应起来
  • sendmail sendmail或其它发信程序的路径

还有个环境变量$USER_EMAIL,也是个发信人,不过是显示在邮件From:里的。如果要设置邮件里的From:,需要自己修改post-receive-email或在其之前执行的脚本,设置环境变量:

export USER_EMAIL="GIT <mailsender@domain.tld>"

这一版修改中作者还去掉了原来的emailprefix设置参数,固定为用仓库里description文件的内容加上[]替代。

最后,在git(git服务器运行用户)用户的HOME下放一个.msmtprc,配置发信认证信息:

defaults
    tls_trust_file /etc/ssl/certs/ca-certificates.crt
account gmail
    host smtp.gmail.com
    port 465
    auth on
    tls on
    tls_starttls off
account mailsender@domain.tld : gmail
#   from must be same as From: in mail, exclude Name, only mail address
#   Because maybe msmtp select account using from
    from mailsender@domain.tld
    user "mailsender@domain.tld"
    password my_passwd
account default : mailsender@domain.tld

现在就可以测试push,看自动发信是否正常了。如果提示functions语法错误,比如:

hooks/functions: 213: Syntax error: Bad for loop variable

可以把这两个脚本第一行的#!/bin/sh改为#!/bin/bash

注意msmtp调用的是提交动作所属用户的.msmtprc,从本机提交和远程ssh进来可能用的就不是一个.msmtprc了。

参考

[Mutt]用msmtp替代esmtp作发信代理

刚开始使用mutt的时候,由于也没有什么经验,在MDA选择方面抓了一个esmtp就用上了。说实话,esmtp也一直工作的很好,就是发信的时候需要傻傻等着(参见muttrc的sendmail_wait参数),如果不等待也可以,发送失败你就不知道了。虽然失败的情况比较少,一般都是邮件比较大或者网络不太好的情况才会发生,但我们外发的邮件往往都是比较重要的,真想有一种能够自动重试的发信代理,于是就查到了msmtp。

msmtpesmtp两个软件作者都不多,但msmtp确实更新频繁一些,而esmtp作者说现在已经不需要怎么添加功能了,只是维护bug。在多账户配置方面,好像msmtp的配置方法要“聪明”一些,账号规则有一些”继承”的小把戏。在发信队列方面,esmtp在Features中明显声明不支持,而msmtp有一个msmtpqueue,国内用的人非常少,只看到cu blog上提到了一点,smtpqueue能够把要外发的邮件集中在一起,单独发送,但没有说失败处理的事情;再到msmtp在sf.net上的cvs中查msmtpqueue的README文件,终于搞清楚了,和我的需要比较吻合。

msmtpqueue会”冒充”成一个MDA,当mutt发信时,它接收外发邮件并返回成功信息,而实际上只是把信件保存在QUEUEDIR里了,当调用msmtp-runqueue.sh的时候,再进行实际的发送处理。发送成功一封信,就会从QUEUEDIR中删除,发送失败就继续保持不动,下次再调用msmtp-runqueue.sh的时候自然又会处理(相当于重发)了。比起mutt中的邮件失败重发还会多生成一个邮件副本(我设定了发信的同时record在当前目录)应该是好多了。

Mails sent successfully will be deleted from the queue directory.
Mails whose delivery failed will be left untouched; you may want to edit
them, delete them by hand or simply run msmtp-runqueue.sh at a later time.

工作开始,首先通过apt安装msmtp,很小的,加上TLS/SSL所需的libgsasl7,一共才182k。然后配置账号文件~/.msmtprc,格式和~/.esmtprc比较类似:

# Default value for all following accounts
defaults
#      tls on
    tls_trust_file /etc/ssl/certs/ca-certificates.crt
#   keepbcc on
    logfile ~/log/msmtp.log

# Gmail
account gmail
    host smtp.gmail.com
    port 587
    auth on
    tls on

account mailbox@gmail.com : gmail
    from mailbox@gmail.com
    user "mailbox@gmail.com"
    password "secret"

# Another mail service
account another_mailbox@domain.com
    host smtp.domain.com
    auth on
    from another_mailbox@domain.com
    user "another_mailbox"
    password "another_secret"

# Default account
account default : mailbox@gmail.com

Copy自官方文档,语法很简单,最开始的defaults是默认设置,然后的account就是各个账号的设置了,账号名称最好和邮件中的From:对应,这样msmtp就能知道调用哪个账号了。最有意思的是,account可以用冒号:来”继承”其它已定义账号的设置,使用这个机制,就为所有gmail邮箱指定了host、port、auth、tls四个属性,然后在具体的账号设置中,只要再设定from、user、password就可以了,很方便,”继承”多个账号的话用逗号隔开。最后默认发信账号(匹配寻找account失败时使用)default可以直接”继承”某个账号的设置过来。

然后设置~/.muttrc,用msmtp来发信:

set sendmail="/path/to/msmtp"
set use_from=yes
set from="Fwolf <mailbox@domain.com>"
set envelope_from=yes
set sendmail_wait=0

测试一下,发信正常,现在可以让msmtpqueue出场了。由于msmtpqueue只是在”转达”msmtp发信的命令,所以需要先把msmtp调试好以后再配置。

msmtpqueue已经随msmtp安装上了,放在/usr/share/doc/msmtp/examples下,按照这里README文件的说明,把msmtp-enqueue.shmsmtp-runqueue.sh两个文件加上执行属性,然后ln到$HOME/.mutt下,再在这里创建一个新目录msmtpqueue,ln成~/.msmtpqueue(两个脚本默认的QUEUEDIR),最后修改muttrc中的sendmail:

set sendmail = "~/.mutt/msmtp-enqueue.sh"

好了,现在再来发信,是不是瞬间就完成了?再看~/.mutt/msmtpqueue目录下,也多了两个文件,分别以.mail.msmtp结尾,他们分别包含了刚才所发信件的信件内容和发送命令。现在运行msmtp-runqueue.sh

~/.mutt$ ./msmtp-runqueue.sh 
*** Sending 2008-01-12-23.03.52.mail...
2008-01-12-23.03.52.mail sent successfully

邮件就这样顺利的发出去了。

工作到这里基本就都完成了,发信时按y嗖一下就完成的感觉真不错,为了更方便的发送邮件,还可以在mutt中定义启动邮件发送程序的快捷键:

macro generic S "!~/.mutt/msmtp-runqueue.sh\n"

ubuntu的包里没有,但svn上还有一个msmtp-listqueue.sh脚本,列出当前QUEUEDIR中信件的From/To/主题,凑合能用,但用RFC2047编码方式的中文标题自然是不能正常显示的了。

Update @ 2009-03-27

知道gmail的两个端口,465和587有区别么?还真不完全一样,我用msmtp试出来的结果是这样的:

  • 如果使用465端口,则置tls_starttls off
  • 如果使用587端口,则置tls_starttls on

否则不是连不上就是无回馈数据。是看了标题: 我翻译的 msmtp 的手册页后受启发发现的。

终于能够通过phpmailer使用gmail账号发送邮件了

phpmailer(现在的版本是1.73)是一个很好用的工具,可以很方便的使用php语言发送邮件,支持smtp及验证,我们一直都用它。

但是,由于gmail的smtp采用了ssl连接:

Outgoing Mail (SMTP) Server – requires TLS: smtp.gmail.com (use authentication) Use Authentication: Yes Use STARTTLS: Yes (some clients call this SSL) Port: 465 or 587

使用phpmailer就无法正常连接gmail的发信服务器了,并且这个问题一直没有得到phpmailer的官方解决,不过在sf.net上面的讨论里倒是找到了一点资料,采用下面这种方法就可以连接gmail了。

修改class.smtp.php,第101行,把

$this->smtp_conn = fsockopen($host, # the host of the server

改成

$this->smtp_conn = fsockopen(‘ssl://’ . $host, # the host of the server

这样就可以了,就是指定使用ssl协议连接主机,当然php的openssl模块必须打开:

extension=php_openssl.dll

但是这样就不能使用非ssl的主机了,我也不像在include目录下面放两份phpmailer,于是,又琢磨出了一种更好的方式:

打开class.phpmailer.php,在大概543行附近,函数SmtpConnect()中,找到:

$this->smtp->do_debug = $this->SMTPDebug; $hosts = explode(“;”, $this->Host); $index = 0; $connection = ($this->smtp->Connected()); // Retry while there is no connection while($index < count($hosts) && $connection == false) { if(strstr($hosts[$index], ":")) list($host, $port) = explode(":", $hosts[$index]); else { $host = $hosts[$index]; $port = $this->Port; }

这一段的部分功能就是,如果你输入的主机名中带有端口号,自动的识别出来,设想虽好,但就是这部分让我们无法直接在调用的时候使用ssl格式的主机名,因为“ssl://xxx”的形式会被误认为是主机:端口号的形式,另外端口号一般都比较固定,我们手工设置好了,也不必一定要和主机一起赋值,所以在上面的代码后面添加:

//Modify by Fwolf @ 2006-4-14, to enable ssl mail connection $host = $this->Host; $port = $this->Port;

就可以了,使用正常smtp主机的时候,用法不变,使用gmail的时候,使用ssl://smtp.gmail.com作为主机名就可以了,唯一稍微麻烦一些的就是端口需要手工指定——其实也不麻烦。

按照上面的配置更改后,使用gmail账号发送邮件时还会有一条警告信息:

Warning: fgets(): SSL: fatal protocol error in H:\php_includes\phpmailer_ssl\cla ss.smtp.php on line 1024

这各警告信息在php的帮助里面有,好像是在使用ssl连接的时候,读文件读到文件尾的时候出现的问题,需要手工屏蔽,或者人为控制读的长度,我们用最简单的方式禁止警告信息显示就可以了,找到class.smtp.php的1024行,在fgets($this->smtp_conn,515)函数前面加上个@就可以了。

下面是一个完整的使用phpmailer发送邮件的函数:

function send_mail($to_address, $to_name ,$subject, $body, $attach = ”) { //使用phpmailer发送邮件 require_once(“phpmailer/class.phpmailer.php”); $mail = new PHPMailer(); $mail->IsSMTP(); // set mailer to use SMTP $mail->CharSet = ‘utf-8’; $mail->Encoding = ‘base64’; $mail->From = ‘fwolf.mailagent@gmail.com’; $mail->FromName = ‘Fwolf’; //$mail->Sender = ‘fwolf.mailagent@gmail.com’; //$mail->ConfirmReadingTo = ‘fwolf.mailagent@gmail.com’; //回执? $mail->Host = ‘ssl://smtp.gmail.com’; $mail->Port = 465; //default is 25, gmail is 465 or 587 $mail->SMTPAuth = true; $mail->Username = “fwolf.mailagent@gmail.com”; $mail->Password = “xxx”; $mail->>ddAddress($to_address, $to_name); //$mail->AddReplyTo(‘fwolf.mailagent@gmail.com’, “Fwolf”); //针对gmail无用,gmail是In-Reply-To:,phpmailer默认生成的是Reply-to: $mail->WordWrap = 50; if (!empty($attach)) $mail->AddAttachment($attach); $mail->IsHTML(false); $mail->Subject = $subject; $mail->Body = $body; //$mail->AltBody = “This is the body in plain text for non-HTML mail clients”; if(!$mail->Send()) { echo “Mail send failed.\r\n”; echo “Error message: ” . $mail->ErrorInfo . “\r\n”; return false; } else { echo(“Send $attach to $to_name <$to_address> successed.\r\n”); return true; } //echo “Message has been sent”; // }

update @ 2006-11-3

感谢RainChen提供的fgets出错提示的更好解决办法(位置大概在class.smtp.php的1024行): 将:

fgets($this->smtp_conn,515)

改为:

!feof($this->smtp_conn) && $str = fgets($this->smtp_conn,515)

这样的代码就规范多了,极力不提倡使用@来屏蔽错误信息。 (未测试, 但有网友反映这种改法是不行的)

Update @ 2008-04-14

试了最新的phpMailer 2.1.0 Beta 2,不用作任何修改,就可以用gmail账号发送邮件了,官方文档中还给出了例子,和我用的方法不太一样。

不过中文附件的问题依然没有解决,需要自己hack。