前一阵子做项目的时候遇到一个功能需求:当程序异常或者重大事件时候,发送邮件通知管理员。按理说这是一个很简单的需求,但是在开发当中遇到了一个问题:因为客户那里的网络拓扑是一个需要设置代理才可以访问外网smtp服务器的网络环境,所以程序在直连外网时候好使,拿到内网就不能用了。于是我在网上找了很多关于使用C#程序发邮件的例子,但很少有关于使用代理方式,特别是支持审核代理方式发送的案例,我分别使用了SMTPClient对象,CDO对象来进行开发,发现.net framework提供的smtpclient对象不支持代理方式发送,cdo里面有些关于代理的设置,但是没有关于访问代理时候的用户名、密码、端口设置的地方,于是问题被搁置下来。后来也请求过微软方面的支持,也没有给出什么好的方案【想让我用webservice,因为那个支持代理的审核,但是既然我能在内网使用foxmail,outlook通过设置代理属性后正常的收发邮件,那为什么还要使用什么webservice呢?看来微软的这些所谓的专家也有很菜的方面】。既然高级的方式使用不了,于是考虑回到原点——使用socket方式发送邮件。废话少说吧,先把代码贴出来大家分享:
1.先声明一个TCPClient对象,用于Socket发送
- private TcpClient sendTcp = null;
2.写几个方法用于与SMTP服务器的交互
- private void MailSocketAlternation(string[] mailto,string subject, string msg, string attachpath)
- {
- bool check = false;
- NetworkStream stream = sendTcp.GetStream();
- #region 发送Hello握手
- string hostName = Dns.GetHostName();
- check = SendCommand(ref stream, "EHLO " + hostName, "EHLO", "250");
- int round = 0;
- //失败重试
- while (!check && round < 5)
- {
- round++;
- check = SendCommand(ref stream, "EHLO " + hostName, "EHLO", "250");
- }
- #endregion
- #region 请求审核登录
- check = SendCommand(ref stream, "AUTH LOGIN ", "AUTH LOGIN", "334");
- round = 0;
- while (!check && round < 5)
- {
- round++;
- check = SendCommand(ref stream, "AUTH LOGIN ", "AUTH LOGIN", "334");
- }
- #endregion
- #region 身份验证
- check = SendCommand(ref stream, Convert.ToBase64String(Encoding.Default.GetBytes(clsParam.Param.SendMailAccount)), "用户名", "334");
- round = 0;
- while (!check && round < 5)
- {
- round++;
- check = SendCommand(ref stream, Convert.ToBase64String(Encoding.Default.GetBytes(clsParam.Param.SendMailAccount)), "用户名", "334");
- }
- if (!check)
- {
- throw new Exception("邮件帐户身份验证失败!");
- }
- check = SendCommand(ref stream, Convert.ToBase64String(Encoding.Default.GetBytes(clsParam.Param.SendMailPWD)), "密码", "235");
- round = 0;
- while (!check && round < 5)
- {
- round++;
- check = SendCommand(ref stream, "EHLO " + hostName, "EHLO", "250");
- int round0 = 0;
- while (!check && round0 < 5)
- {
- round0++;
- check = SendCommand(ref stream, "EHLO " + hostName, "EHLO", "250");
- }
- check = SendCommand(ref stream, "AUTH LOGIN ", "AUTH LOGIN", "334");
- check = SendCommand(ref stream, Convert.ToBase64String(Encoding.Default.GetBytes(clsParam.Param.SendMailAccount)), "用户名", "334");
- check = SendCommand(ref stream, Convert.ToBase64String(Encoding.Default.GetBytes(clsParam.Param.SendMailPWD)), "密码", "235");
- }
- if (!check)
- {
- throw new Exception("邮件帐户身份验证失败!");
- }
- #endregion
- #region 发件人
- check = SendCommand(ref stream, "MAIL FROM:<" + clsParam.Param.SendMailAccount + ">", "MAIL FROM", "250");
- round = 0;
- while (!check && round < 5)
- {
- round++;
- check = SendCommand(ref stream, "MAIL FROM:<" + clsParam.Param.SendMailAccount + ">", "MAIL FROM", "250");
- }
- #endregion
- #region 收件人
- check = SendCommand(ref stream, "RCPT TO:<" + mailto[0] + ">", "RCPT TO", "250");
- round = 0;
- while (!check && round < 5)
- {
- round++;
- check = SendCommand(ref stream, "RCPT TO:<" + mailto[0] + ">", "RCPT TO", "250");
- }
- #endregion
- #region 抄送人
- if (mailto.Length > 1)
- {
- for (int i = 1; i < mailto.Length; i++)
- {
- check = SendCommand(ref stream, "RCPT TO:<" + mailto[i] + ">", "RCPT TO", "250");
- round = 0;
- while (!check && round < 5)
- {
- round++;
- check = SendCommand(ref stream, "RCPT TO:<" + mailto[i] + ">", "RCPT TO", "250");
- }
- }
- }
- #endregion
- #region 密送人
- //这里看大家是否需要了,可以偷偷给自己发一份,呵呵
- #endregion
- #region 请求发送邮件体
- check = SendCommand(ref stream, "DATA", "DATA", "354");
- round = 0;
- while (!check && round < 5)
- {
- round++;
- check = SendCommand(ref stream, "DATA", "DATA", "354");
- }
- #endregion
- #region 发送邮件头
- StringBuilder mailhead = new StringBuilder();
- mailhead.Append("Subject: " + subject)
- .Append("/nDate: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"))
- .Append("/nFrom: " + "上报软件<" + clsParam.Param.SendMailAccount + ">")
- .Append("/nTo: "+mailto[0]);
- if (mailto.Length > 1)
- {
- mailhead.Append("/nCc: ");
- for (int i = 1; i < mailto.Length; i++)
- {
- mailhead.Append(mailto[i]+";");
- }
- }
- mailhead.Append("/nBcc: a@b.com/n/n"); //这个地方大家随便改,注意这只是头,而不是真正的接收者,客户端只会解释,而不会发送,发送的是完全由RCPT TO控制的
- #endregion
- #region 发送邮件内容
- check = SendCommand(ref stream, mailhead.ToString()
- + msg
- + "/n/r/n./r/n", "信已发出,服务器", "250", false);
- round = 0;
- while (!check && round < 5)
- {
- round++;
- check = SendCommand(ref stream, mailhead.ToString()
- + msg
- + "/n/r/n./r/n", "信已发出,服务器", "250", false);
- }
- #endregion
- }
- private bool SendCommand(ref NetworkStream netstream, string content, string rehead, string reflag)
- {
- return SendCommand(ref netstream, content, rehead, reflag, true);
- }
- private bool SendCommand(ref NetworkStream netstream, string content, string rehead, string reflag, bool isNewLine)
- {
- bool retBool = false;
- try
- {
- WriteToNetStream(ref netstream, content, isNewLine);
- string come = ReadFromNetStream(ref netstream);
- AddLog(rehead + "应答:" + come + "/r/n");
- retBool = CheckForError(come, reflag);
- }
- catch
- {
- retBool = false;
- }
- return retBool;
- }
- private void WriteToNetStream(ref NetworkStream NetStream, string Command)
- {
- WriteToNetStream(ref NetStream, Command, true);
- }
- private void WriteToNetStream(ref NetworkStream NetStream, string message, bool isNewLine)
- {
- string stringToSend = isNewLine ? message + "/r/n" : message;
- byte[] arrayToSend = Encoding.Default.GetBytes(stringToSend.ToCharArray());
- NetStream.Write(arrayToSend, 0, arrayToSend.Length);
- }
- private string ReadFromNetStream(ref NetworkStream NetStream)
- {
- byte[] temp = new byte[512];
- NetStream.Read(temp, 0, temp.Length);
- return Encoding.Default.GetString(temp);;
- }
- private bool CheckForError(string strMessage, string check)
- {
- if (strMessage.IndexOf(check) == -1)
- {
- return false;
- }
- else
- {
- return true;
- }
- }
3.再包一层方法
- private bool SendMail(string subject, string msg, string attachpath)
- {
- bool retbool = false;
- try
- {
- this.btnSendTestMail.Enabled = false;
- string[] mails = this.txtNoticeMailAddress.Text.Split(";".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);//收件人用;隔开
- if (clsParam.Param.IsProxy)
- {
- switch (clsParam.Param.ProxyType)
- {
- case MailProxyType.HTTP:
- #region HTTP方式处理
- if (sendTcp == null)
- {
- string authstr = clsParam.Param.ProxyUID + ":" + clsParam.Param.ProxyPWD;
- string authproxy = "CONNECT " + clsParam.Param.SendMailSmtpAdd + ":" + clsParam.Param.SendMailSmtpPort
- + " HTTP/1.0/r/nProxy-Authorization: Basic "
- + Convert.ToBase64String(Encoding.Default.GetBytes(authstr)) + "/r/n/r/n";//代理服务器审核
- bool testproxyflag = true;//测试时候使用,设置成false可以不使用代理的Socket方式发邮件
- if (testproxyflag)
- {
- sendTcp = new TcpClient(clsParam.Param.ProxyIP, clsParam.Param.ProxyPort);
- }
- else
- {
- sendTcp = new TcpClient(clsParam.Param.SendMailSmtpAdd, clsParam.Param.SendMailSmtpPort);//注释1
- }
- NetworkStream stream = sendTcp.GetStream();
- //发送代理验证
- if (testproxyflag)
- {
- WriteToNetStream(ref stream, authproxy, false);//注释2
- }
- //获取验证反馈
- string response = ReadFromNetStream(ref stream);
- AddLog("连接应答:" + response + "/r/n");
- bool check = false;
- if (testproxyflag)
- {
- check = CheckForError(response, "HTTP/1.0 200") ||
- CheckForError(response, "HTTP/1.1 200");
- if (check)
- {
- AddLog("邮件代理连接成功/r/n");
- }
- else
- {
- throw new Exception("邮件代理连接失败/r/n");
- }
- string receive = ReadFromNetStream(ref stream);//这个一定要有,自己在这里走了点弯路
- AddLog("远端服务器连接应答:" + receive + "/r/n");
- check = CheckForError(receive, "220");
- if (check)
- {
- AddLog("远端服务器连接成功/r/n");
- }
- else
- {
- throw new Exception("远端服务器连接失败/r/n");
- }
- }
- else
- {
- check = CheckForError(response, "220");//注释3
- if (check)
- {
- AddLog("邮件服务器连接成功/r/n");
- }
- else
- {
- throw new Exception("邮件服务器连接失败/r/n");
- }
- }
- }
- try
- {
- //发送邮件
- MailSocketAlternation(mails, subject, msg, attachpath);
- }
- catch(Exception ex)
- {
- sendTcp.Close();
- sendTcp = null;
- throw ex;
- }
- finally
- {
- if (sendTcp != null)
- {
- try
- {
- NetworkStream stream = sendTcp.GetStream();
- WriteToNetStream(ref stream, "QUIT");
- AddLog("QUIT应答:" + ReadFromNetStream(ref stream) + "/r/n");
- }
- catch (Exception ex)
- {
- throw ex;
- }
- finally
- {
- sendTcp.Close();
- sendTcp = null;
- }
- }
- }
- #endregion
- break;
- case MailProxyType.SOCKS4: //此种方式我软件没让他支持,如果大家想用可以使用CDO的方法试一下,我也没有试是否可行,但是Socket的方式肯定是可以的,只是要有一些变化,如果有人感兴趣可以给我留言。使用CDO需要在项目里引用Microsoft CDO for Windows 2000 Library和Microsoft ActiveX Data Objects 2.8 Library两个COM
- #region SOCKS4方式处理--暂时不予支持
- //CDO.Message oMsg = new CDO.Message();
- //oMsg.From = clsParam.Param.SendMailAccount;
- //oMsg.To = mails[0];
- //oMsg.Subject = subject;
- //oMsg.HTMLBody = msg;
- //if (File.Exists(attachpath))
- //{
- // oMsg.AddAttachment(attachpath, "", "");
- //}
- //CDO.IConfiguration iConfg = oMsg.Configuration;
- //ADODB.Fields oFields = iConfg.Fields;
- //oFields["http://schemas.microsoft.com/cdo/configuration/sendusing"].Value = 2;
- //oFields["http://schemas.microsoft.com/cdo/configuration/smtpauthenticate"].Value = 1;
- value=0 代表Anonymous验证方式(不需要验证)
- value=1 代表Basic验证方式(使用basic (clear-text) authentication.
- The configuration sendusername/sendpassword or postusername/postpassword fields are used to specify credentials.)
- Value=2 代表NTLM验证方式(Secure Password Authentication in Microsoft Outlook Express)
- //oFields["http://schemas.microsoft.com/cdo/configuration/smtpserver"].Value = clsParam.Param.SendMailSmtpAdd;
- //oFields["http://schemas.microsoft.com/cdo/configuration/sendemailaddress"].Value = clsParam.Param.SendMailAccount; //sender mail
- //oFields["http://schemas.microsoft.com/cdo/configuration/sendusername"].Value = clsParam.Param.SendMailAccount;
- //oFields["http://schemas.microsoft.com/cdo/configuration/sendpassword"].Value = clsParam.Param.SendMailPWD;
- //oFields["http://schemas.microsoft.com/cdo/configuration/smtpconnectiontimeout"].Value = 5;
- //oFields["http://schemas.microsoft.com/cdo/configuration/languagecode"].Value = 0x0804;
- 代理设置
- oFields["http://schemas.microsoft.com/cdo/configuration/urlproxyserver"].Value = "182.1.1.200";
- oFields["http://schemas.microsoft.com/cdo/configuration/proxyserverport"].Value = 8080;
- //oFields.Update();
- //oMsg.BodyPart.Charset = "gb2312";
- //oMsg.HTMLBodyPart.Charset = "gb2312";
- //oMsg.Send();
- //oMsg = null;
- #endregion
- break;
- case MailProxyType.SOCKS5: //同Socket4的注释
- #region SOCKS5方式处理--暂时不予支持
- #endregion
- break;
- }
- }
- else
- { //这种方式我也列出来,方便大家参考,如果能直连外网,这个方式最简单
- #region 标准方式发送邮件
- System.Net.Mail.MailMessage mail = new System.Net.Mail.MailMessage();
- SmtpClient SmtpServer = new SmtpClient();
- SmtpServer.Credentials = new System.Net.NetworkCredential(clsParam.Param.SendMailAccount, clsParam.Param.SendMailPWD);
- SmtpServer.Port = 25;
- SmtpServer.Host = clsParam.Param.SendMailSmtpAdd;
- SmtpServer.EnableSsl = false;
- mail.From = new MailAddress(clsParam.Param.SendMailAccount, "上报软件", System.Text.Encoding.Default);
- mail.To.Add(mails[0]);
- if (mails.Length > 1)
- {
- for (int i = 1; i < mails.Length; i++)
- {
- mail.CC.Add(mails[i]);
- }
- }
- mail.Bcc.Add("a@b.com");
- if (File.Exists(attachpath))
- {
- mail.Attachments.Add(new Attachment(attachpath));
- }
- mail.IsBodyHtml = false;
- mail.BodyEncoding = Encoding.Default;
- mail.SubjectEncoding = Encoding.Default;
- mail.DeliveryNotificationOptions = DeliveryNotificationOptions.OnFailure;
- mail.Subject = subject;
- mail.Body = msg;
- SmtpServer.Send(mail);
- #endregion
- }
- AddLog("邮件发送成功");
- retbool = true;
- }
- catch (Exception ex)
- {
- AddLog("发送邮件异常:" + ex.Message);
- retbool = false;
- }
- finally
- {
- this.btnSendTestMail.Enabled = true;
- }
- return retbool;
- }
4.下面我们测试一下
- private void btnSendTestMail_Click(object sender, EventArgs e)
- {
- if (SendMail("【系统自动邮件】系统运行情况通知", "这是一封测试邮件", null))
- {
- MessageBox.Show("测试邮件发送成功!", "", MessageBoxButtons.OK, MessageBoxIcon.Information);
- }
- else
- {
- MessageBox.Show("测试邮件发送失败!", "", MessageBoxButtons.OK, MessageBoxIcon.Error);
- }
- }
程序写的匆忙,注释少了一些,不过应该还方便看出思路,上面我分别列出了三种发送邮件的方式,其中Socket的方式又可以采用三种代理方式来搞定(我只列出了一种HTTP方式的,另两种可以和我联系)。关于SMTP交互的方式,我参考了RFC821的文档,如果E文不好,可以参考中文的,对于发送方法还借鉴了微软社区的这篇文章。另外,对于通过审核代理方式可以参考这篇网文,关于CdoConfiguration Module可以参考这篇资料。在Socket方法里我没有实现附件的功能,因为我那里不需要,如果大家想用,可以留言联系。
希望这些代码可以对一些有相同问题困扰的Coder们有所帮助,有问题大家留言探讨。
昨天刚放到blog上,就有人转贴了http://www.diybl.com/course/4_webprogram/asp.net/asp_netshl/20081117/151850.html,连笔误都搞走了,呵呵。转帖可以,但是希望能够附上我的blog地址,谢谢!
=========================