为Jmail.net增加ReplyTO功能并加快发送多封邮件速度

    首先声明:本文的内容及所涉及的技术,仅做学习和技术研究,并不涉及任何对相关公司软件版权的有意侵犯。

    放长假以来,天天在琢磨邮件的事,不过通过这几天也还是认真的学习和了解了一下SMTP协议和指令,对邮件发送的各个方面也有了一些了解,特别是自己编写的那个邮件发送平台小软件为了做的更好,不得不一次次深入的学习,自己的能力也得到了提高。

    在邮件发送小程序的编写过程中,我做了几次测试,发现邮件的发送速度都很慢,平均下来要15秒到16秒才能发出一封,这样的话,如果我某个时段有100封邮件需要发送,那差不多要半个多小时,用手机发短信都用不了那么长时间呀。一开始我以为是自己的代码有问题,仔细检查后没有发现问题。下在纠结的时候,一次偶然的测试时,我没有使用公司的SMTP服务器,而是使用了新浪的smtp.sina.com服务器,这时发现邮件发送的速度约为1至2秒一封,问题找到了,原来是出在公司的邮件服务器上。

    公司的邮件服务器使用的是Lotus Notes 8.0,一台小型机,四千左右用户,从理论上来说,不应该这么慢。与邮件服务器管理员联系,请他在后台监测一下发送的过程,他监测后告诉我,每发送一封邮件,都要连接一次服务器,而我们的服务器前段时间刚做了一次安全基线,加上了一个参数,“用于验证的 SMTP 会话发送的邮件是否来自通过验证的用户的 Internet 地址,此设置的目的是确认用户没有尝试伪造“收件人”域”。这有可能是造成每次连接服务器验证时间过长的情况。相关信息如下:


[454808:00136-03743] 2010-10-08 13:32:13 SMTP Server: localhost (10.169.**.**) connected
[454808:00136-03743] 2010-10-08 13:32:13 SMTP Server: Authentication succeeded for user ******* ; connecting host 10.169.**.**
[454808:00136-02218] 2010-10-08 13:32:13 SMTP Server: Message 001E6A8C (MessageID: ) received
[454808:00136-03743] 2010-10-08 13:32:13 SMTP Server: localhost (10.169.**.**) disconnected. 1 message[s] received
[364650:00020-04113] 2010-10-08 13:32:13 Router: Message 001E6A8C delivered to *******

[454808:00136-02218] 2010-10-08 13:32:30 SMTP Server: localhost (10.169.**.**) connected
[454808:00136-02218] 2010-10-08 13:32:30 SMTP Server: Authentication succeeded for user ******* ; connecting host 10.169.**.**
[454808:00136-03743] 2010-10-08 13:32:30 SMTP Server: Message 001E710C (MessageID: ) received
[454808:00136-02218] 2010-10-08 13:32:30 SMTP Server: localhost (10.169.**.**) disconnected. 1 message[s] received
[364650:00016-03085] 2010-10-08 13:32:30 Router: Message 001E710C delivered to *******

    如果问题的确是出在每次连接验证的时候时间过长的话,如果是发送多封邮件,是否可以验证一次后连续发送,最后再关闭呢?认真的查阅了Jmail.Net的各个参数,发现Jmail.Net并没有提供这方面的功能,它只能每次发送一封邮件时连接一次服务器,它自身的群组邮件功能也不适用于我想达到的每封邮件内容不相同的目的,在网上搜索了一些相当内容也没有合适的,最后决定自己动手改造Jmail.Net。

    下载到一个Jmail.Net的反编译源码(需要的网友请自行搜索下载,这里就不给出下载地址了),装入VS2005,开始阅读代码。

    从编写代码时的自动提示就可以看出,发送的代码由Dimac.Jmail.Smtp.Send()来实现,直接查看Dimac.JMail.Smtp项目下的Smtp.cs代码。定位到最主要的功能实现代码上:

        public void Send(Message message)
        {
            if (message == null)
            {
                throw new ArgumentNullException("message", "Message cannot be null.");
            }
            using (SmtpClient client = new SmtpClient(this.m_logStream))
            {
                client.Connect(this.m_hostName, this.m_port);
                client.Helo(this.m_domain);
                client.Auth(this.m_authentication, (this.m_userName == null) ? string.Empty : this.m_userName, (this.m_password == null) ? string.Empty : this.m_password);
                if (message.From != null)
                {
                    client.MailFrom(message.From);
                }
                foreach (Address address in message.To)
                {
                    client.RcptTo(address.Email);
                }
                foreach (Address address2 in message.Cc)
                {
                    client.RcptTo(address2.Email);
                }
                foreach (Address address3 in message.Bcc)
                {
                    client.RcptTo(address3.Email);
                }
                client.Data(message);
                client.Quit();
                
            }
        }

    根据我的想法,我要将这个代码分成三个不同的子程序,分别为Open(打开端口并验证)、Send(发送邮件)、Close(关闭连接),这些工作就需要对现有的代码进行修改,中间出现的问题主要是一开始没有注意,每个子程序里面都使用了一次using (client = new SmtpClient);,这样的话就子程序执行完成后,client变量也就释放了,达不到想要的效果,后来搜索并查阅了一些相关资料,在Smtp.cs中加入以下代码:

        private SmtpClient m_Client = null;

        // Methods


        public void Open(string hostName, short port, string domain, SmtpAuthentication authentication, string userName, string password)
        {


                if (null == m_Client)
                {
                    m_Client = new SmtpClient();
                    m_Client.Connect(hostName, port);
                    m_Client.Helo(domain);
                    m_Client.Auth(authentication, (userName == null) ? string.Empty : userName, (password == null) ? string.Empty : password);
                }
            
        }

        public void Close()
        {
            System.Diagnostics.Debug.Assert(null != m_Client);
            if (null != m_Client)
                m_Client.Quit();
        }

        public void SendEx(Message message)
        {
            System.Diagnostics.Debug.Assert(null != m_Client);
            //            if (null == m_Client)
            //return false;
            if (message == null)
            {
                throw new ArgumentNullException("message", "Message cannot be null.");
            }
            if (message.From != null)
            {
                m_Client.MailFrom(message.From);
            }
            foreach (Address address in message.To)
            {
                m_Client.RcptTo(address.Email);
            }
            foreach (Address address2 in message.Cc)
            {
                m_Client.RcptTo(address2.Email);
            }
            foreach (Address address3 in message.Bcc)
            {
                m_Client.RcptTo(address3.Email);
            }
            m_Client.Data(message);
        }

    这样一来,我就将邮件的发送过程分成了三步,先打开端口,然后发送邮件(可以是多封循环发送),最后关闭端口,大大的降低了发送多封邮件的时间,实现了邮件发送的一次验证,多封发送功能。同时我也保留了官方的发送方式,以方便标准的调用和单独发送一封邮件的情况。下面是二种方式在公司邮件服务器上的对比效果,可以看到速度已经极为加快。

    这个功能修改成功后,我不由的又动起了心思,记得在上一篇文章中我说到了Jmail 4.4还有的ReplyTo功能,在最新的Jmail.Net中竟然没有了,而我的邮件发送平台也很需要这个功能(A通过平台发送的一封邮件给B,B如果要回复当然最好是直接点击回复按钮就回复到A的邮箱而不是系统发件邮箱了),说干就干,再次分析代码。

    根据我对SMTP指令、信件头内容的了解和使用Jmail.Net的情况,ReplyTo应该加在Dimac.JMail项目的Message.cs中,认真查看了Message.cs的代码,发现还涉及到了Header.cs和HeaderCollection.cs文件,一并进行修改。

    修改的过程主要是类猫画虎,没有什么可说的,但要注意的一点是:在我们平时的使用中习惯用message.replyto来进行操作,但在邮件信头的指令中使用的是“Reply-To:”的指令,中间有减号而且大小写敏感,要注意。

    首先对message.cs文件做以下修改:

//加入以下的代码
        private Address m_replyto;

        public Address ReplyTo
        {
            get
            {
                return this.m_replyto;
            }
            set
            {
                this.m_replyto = value;
            }
        }
//修改public message
        public Message()
            : this(new Address(), null, null, null,null )
        {
        }

        public Message(Address from, Address to, Address replyto,string subject, string bodyText)
        {
            this.m_charset = Encoding.ASCII;
            this.m_from = from;
            this.m_replyto = replyto;
            this.m_to = new AddressList();
            if (to != null)
            {
                this.m_to.Add(to);
            }
            this.m_cc = new AddressList();
            this.m_bcc = new AddressList();
            this.m_subject = subject;
            this.m_date = DateTime.Now;
            this.m_bodyText = bodyText;
            this.m_bodyHtml = null;
            this.m_attachments = new AttachmentCollection();
            this.m_headers = new NameValueCollection();
        }

//修改message.cs文件中的以下三个子程序,可参照m_from对应添加相应的m_replyto:
//public Entity Assemble()/public object Clone()/public static Message Disassemble(Entity entity)

    对HeaderCollection.cs文件进行修改:

//增加以下代码
        public Address Replyto
        {
            get
            {
                object headerValue = this.InternalGetHeaderValue("Replyto");
                if (headerValue == null)
                {
                    headerValue = this.InternalSet("Replyto", new Address()).Value;
                }
                return (Address)headerValue;
            }
            set
            {
                this.InternalSet("Reply-To", value);
            }

        }


//修改以下二个子程序的判断部分
        public ICollection CustomKeys
        {
            get
            {
                ArrayList list = new ArrayList();
                foreach (string str in this.Names)
                {
                    if (((((string.Compare("Content-Type", str, true) != 0) && (string.Compare("Content-Disposition", str, true) != 0)) && ((string.Compare("Content-Transfer-Encoding", str, true) != 0) && (string.Compare("Subject", str, true) != 0))) && (((string.Compare("From", str, true) != 0) && (string.Compare("Reply-To", str, true) != 0) && (string.Compare("Date", str, true) != 0)) && ((string.Compare("To", str, true) != 0) && (string.Compare("Bcc", str, true) != 0)))) && (string.Compare("Cc", str, true) != 0))
                    {
                        list.Add(str);
                    }
                }
                return list;
            }
        }

        public ICollection CustomKeys
        {
            get
            {
                ArrayList list = new ArrayList();
                foreach (string str in this.Names)
                {
                    if (((((string.Compare("Content-Type", str, true) != 0) && (string.Compare("Content-Disposition", str, true) != 0)) && ((string.Compare("Content-Transfer-Encoding", str, true) != 0) && (string.Compare("Subject", str, true) != 0))) && (((string.Compare("From", str, true) != 0) && (string.Compare("Reply-To", str, true) != 0) && (string.Compare("Date", str, true) != 0)) && ((string.Compare("To", str, true) != 0) && (string.Compare("Bcc", str, true) != 0)))) && (string.Compare("Cc", str, true) != 0))
                    {
                        list.Add(str);
                    }
                }
                return list;
            }
        }

    对Header.cs文件进行修改:

//修改程序,增加一个对ReplyTo的处理
        internal Header(HeaderCollection parent, string name, object value)
        {
            this.m_parent = parent;
            this.m_name = name;
            if ((value != null) && (typeof(string) == value.GetType()))
            {
                string contentType = (string)value;
                switch (this.m_name.ToLower())
                {
                    case "content-type":
                        this.m_value = MediaType.Parse(contentType);
                        return;

                    case "content-disposition":
                        this.m_value = Disposition.Parse(parent, contentType);
                        return;

                    case "content-transfer-encoding":
                        this.m_value = TransferEncoding.Parse(contentType);
                        return;

                    case "date":
                        this.m_value = Rfc822Date.Parse(contentType);
                        return;

                    case "from":
                        this.m_value = Address.Parse(contentType);
                        return;

                    case "replyto":
                        this.m_value = Address.Parse(contentType );
                        return;
                    
                    case "to":
                    case "cc":
                    case "bcc":
                        this.m_value = AddressList.Parse(contentType);
                        return;
                }
                this.m_value = value;
            }
            else
            {
                this.m_value = value;
            }
        }

    至此,为Jmail.Net增加ReplyTo功能的修改已经完成,重新生成一下解决方案后,将生成的二个dll文件拷贝到应用程序的bin目录中,重新引用一次后,就可以使用了。

    这次实践,虽然做的不是很多,在高手看来也不值一晒,但最主要的是在实际的设计中考虑到了不足,想到了自己所需要增强的功能,并且通过自己的努力达到了原来的设想,对自己的能力也得到了一个提高。

    BTW:其实我还是感觉发送邮件慢的问题并不是验证,而是从发起连接到连接成功的时间太长,但还没有找到具体的原因,现在的处理方式也已经得到了有效的成果,也就可以了。

 

 

posted on 2010-10-10 15:47  vvian  阅读(1191)  评论(0编辑  收藏  举报