james源码解析(三)

因个人需要,在个人服务器上搭建了邮箱服务器,使用的是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方法中

1
queue.enQueue(mail);

可见最终是交给了一个队列,在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中的一个概念————mailethttps://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应该是处理投递到当前服务器的邮件。那么投递到其他邮箱服务器的实现又在哪里呢?

先看本地投递吧

本地投递没多少特别的,主要是注入了UsersRepositoryMailboxManager负责用户和邮件的存储,实际使用是再委托给了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之类的,不做深入研究


james源码解析(三)
http://blog.inkroom.cn/2022/11/14/2VRR7FK.html
作者
inkbox
发布于
2022年11月14日
许可协议