因个人需要,在个人服务器上搭建了邮箱服务器,使用的是mailu,整体基于docker多容器搭建。
本来用起来没什么大问题,但是因为其自带nginx容器,和我原本部署的nginx容器会有一定的冲突,不是很满意
一番查找后,让我找到了james——apache开发的基于java的邮箱服务器。
上一篇中解析到了SMTP协议的解码和命令执行,姑且搞明白了网络通信的部分,但是没有弄清楚如何跟邮件处理关联的。
在翻看了代码后,让我找到了 DataCmdHandler 类,这是负责Data指令的数据,指令执行完之后就是邮件发送完成了
查看doDATA方法,发现一行代码
1
| session.pushLineHandler(lineHandler);
|
这是在往处理链条里加入一个新的处理器,并且在原有处理器前执行,不知道和之前提到的 获取最后一个LineHandler 有没有关系,继续往里深入,没有发现其被添加到chain里
debug后发现,这个lineHandler是个责任链模式,具体里面有哪些handler暂时不去深究,现在的重点是要找到最终的邮件处理,在一通操作后,找到了DataLineJamesMessageHookHandler,在这个类里面负责了Mail的创建,之前的handler应该就是解析各个部分。
在这个类的 messageHandlers 属性中有个类是 SendMailHandler,在其onMessage方法中
可见最终是交给了一个队列,在jpa版本中这个队列是ActiveMQCacheableMailQueue,就整体架构来看,这应该是一个基于内存的队列。
队列入队的数据,需要通知消费者,相关代码在JMSCacheableMailQueue的第204行,再往下就是activemq-client依赖包提供的内容了。
SMTP邮件投递确实是一个异步的过程,可以使用队列解耦,但是IMAP和pop3是同步的,应该没法用mq了
找到了生产者,之后就是找消费者。debug之后,queueName是 queue://spool ,直接搜这个字符串没找到。那就只能从创建开始,最终找到了JamesMailSpooler,从里面的注释来看,这个类是负责响应队列消息分发给processor
1 2 3 4 5 6 7
| private reactor.core.Disposable run(MailQueue queue) { return Flux.from(queue.deQueue()) .flatMap(item -> handleOnQueueItem(item).subscribeOn(Schedulers.elastic()), configuration.getConcurrencyLevel()) .onErrorContinue((throwable, item) -> LOGGER.error("Exception processing mail while spooling {}", item, throwable)) .subscribeOn(Schedulers.elastic()) .subscribe(); }
|
这里是通过一个定时器去队列里获取消息,最终转发到第117行,给处理器处理消息。从这个流程来看,感觉ActiveMQ仿佛没用上啊
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| private void performProcessMail(MailQueueItem queueItem, Mail mail) { LOGGER.debug("==== Begin processing mail {} ====", mail.getName()); ImmutableList<MailAddress> originalRecipients = ImmutableList.copyOf(mail.getRecipients()); try { mailProcessor.service(mail);
if (Thread.currentThread().isInterrupted()) { throw new InterruptedException("Thread has been interrupted"); } queueItem.done(true); } catch (Exception e) { handleError(queueItem, mail, originalRecipients, e); } finally { LOGGER.debug("==== End processing mail {} ====", mail.getName()); } }
|
现在已经真正开始了邮件的一个处理过程,核心接口是MailProcessor。james把不同邮件状态交给不同的MailProcessor实例执行,从配置文件中也能看出来,抽象父类AbstractStateMailetProcessor负责处理这一逻辑
这里开始涉及james中的一个概念————mailet,https://james.apache.org/server/feature-mailetcontainer.html 有对其详细的描述
我的英文水平一般,看完之后的理解是:mailt是一个邮件处理器的抽象,其由两部分组成————matcher和Processor,前者负责匹配邮件,确定是否需要执行processor,后者负责具体的逻辑。整体看来依然是一个责任链模式或者装饰模式
mailt的实现类非常多,结合mailetcontainer.xml来看,最主要的是ToProcessor类————原本我是这样想的,但是打开代码一看
1 2 3 4 5 6 7 8 9 10
| @Override public void service(Mail mail) throws MessagingException { if (debug) { LOGGER.debug("Sending mail {} to {}", mail, processor); } mail.setState(processor); if (noticeText.isPresent()) { setNoticeInErrorMessage(mail); } }
|
实际上这货就负责改一个邮件状态,然后在流程中就是交给其他处理器负责了。具体是那个处理器可以从配置文件中看到————transport
再重新看配置文件夹,重点是transport
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <processor state="transport" enableJmx="true"> <matcher name="relay-allowed" match="org.apache.james.mailetcontainer.impl.matchers.Or"> <matcher match="SMTPAuthSuccessful"/> <matcher match="SMTPIsAuthNetwork"/> <matcher match="SentByMailet"/> </matcher>
<mailet match="All" class="RemoveMimeHeader"> <name>bcc</name> <onMailetException>ignore</onMailetException> </mailet> <mailet match="All" class="RecipientRewriteTable"> <errorProcessor>rrt-error</errorProcessor> </mailet> <mailet match="RecipientIsLocal" class="Sieve"/> <mailet match="RecipientIsLocal" class="AddDeliveredToHeader"/> <mailet match="RecipientIsLocal" class="LocalDelivery"/> <mailet match="HostIsLocal" class="ToProcessor"> <processor>local-address-error</processor> <notice>550 - Requested action not taken: no such user here</notice> </mailet>
<mailet match="relay-allowed" class="ToProcessor"> <processor>relay</processor> </mailet> </processor>
|
这里有开始看不懂了,Sieve是RFC3028的java实现,是为了实现邮件过滤,可以理解成防垃圾邮件。LocalDelivery应该是处理投递到当前服务器的邮件。那么投递到其他邮箱服务器的实现又在哪里呢?
先看本地投递吧
本地投递没多少特别的,主要是注入了UsersRepository、MailboxManager负责用户和邮件的存储,实际使用是再委托给了MailDispatcher负责。很多项目都会这样,一层套一层,导致理解起来相当费劲。就jpa版本来说,就是把数据入库,要注意的是这里没有做消息通知,说明james的listener机制还有别的地方再处理。搞得很头大啊!
手动发一封到其他邮箱服务的邮件,debug看一下具体流程
果然根本发不出去!
仔细看错误日志,这是在 RCPT TO给拒绝了
检查了半天,最后改了一下 smtpserver.xml 的配置,把587端口的ssl要求都关闭,同时邮件发送脚本改用587端口
debug之后,找到了RemoteDelivery,很明显,这个类负责投递远程邮件。那么这个类又是什么时候注入的呢?
再仔细回去看配置文件,这是在另一个state的processor中配置的
1 2 3 4 5 6 7 8 9 10 11
| <processor state="relay" enableJmx="true"> <mailet match="All" class="RemoteDelivery"> <outgoingQueue>outgoing</outgoingQueue> <delayTime>5000, 100000, 500000</delayTime> <maxRetries>3</maxRetries> <maxDnsProblemRetries>0</maxDnsProblemRetries> <deliveryThreads>10</deliveryThreads> <sendpartial>true</sendpartial> <bounceProcessor>bounces</bounceProcessor> </mailet> </processor>
|
查看该类的实现逻辑,依旧使用了队列,将其投递到了 outgoing 队列中,该队列由DeliveryRunnable负责消费,再将投递操作委托给MailDelivrer,再然后就是一些具体的邮件发送过程了,比如解析dns之类的,不做深入研究