前一阵子做项目的时候遇到一个功能需求:当程序异常或者重大事件时候,发送邮件通知管理员。按理说这是一个很简单的需求,但是在开发当中遇到了一个问题:因为客户那里的网络拓扑是一个需要设置代理才可以访问外网smtp服务器的网络环境,所以程序在直连外网时候好使,拿到内网就不能用了。于是我在网上找了很多关于使用C#程序发邮件的例子,但很少有关于使用代理方式,特别是支持审核代理方式发送的案例,我分别使用了SMTPClient对象,CDO对象来进行开发,发现.net framework提供的smtpclient对象不支持代理方式发送,cdo里面有些关于代理的设置,但是没有关于访问代理时候的用户名、密码、端口设置的地方,于是问题被搁置下来。后来也请求过微软方面的支持,也没有给出什么好的方案【想让我用webservice,因为那个支持代理的审核,但是既然我能在内网使用foxmail,outlook通过设置代理属性后正常的收发邮件,那为什么还要使用什么webservice呢?看来微软的这些所谓的专家也有很菜的方面】。既然高级的方式使用不了,于是考虑回到原点——使用socket方式发送邮件。废话少说吧,先把代码贴出来大家分享:
1.先声明一个TCPClient对象,用于Socket发送
- privateTcpClientsendTcp=null;
2.写几个方法用于与SMTP服务器的交互
- privatevoidMailSocketAlternation(string[]mailto,stringsubject,stringmsg,stringattachpath)
- {
- boolcheck=false;
- NetworkStreamstream=sendTcp.GetStream();
- #region发送Hello握手
- stringhostName=Dns.GetHostName();
- check=SendCommand(refstream,"EHLO"+hostName,"EHLO","250");
- intround=0;
- //失败重试
- while(!check&&round<5)
- {
- round++;
- check=SendCommand(refstream,"EHLO"+hostName,"EHLO","250");
- }
- #endregion
- #region请求审核登录
- check=SendCommand(refstream,"AUTHLOGIN","AUTHLOGIN","334");
- round=0;
- while(!check&&round<5)
- {
- round++;
- check=SendCommand(refstream,"AUTHLOGIN","AUTHLOGIN","334");
- }
- #endregion
- #region身份验证
- check=SendCommand(refstream,Convert.ToBase64String(Encoding.Default.GetBytes(clsParam.Param.SendMailAccount)),"用户名","334");
- round=0;
- while(!check&&round<5)
- {
- round++;
- check=SendCommand(refstream,Convert.ToBase64String(Encoding.Default.GetBytes(clsParam.Param.SendMailAccount)),"用户名","334");
- }
- if(!check)
- {
- thrownewException("邮件帐户身份验证失败!");
- }
- check=SendCommand(refstream,Convert.ToBase64String(Encoding.Default.GetBytes(clsParam.Param.SendMailPWD)),"密码","235");
- round=0;
- while(!check&&round<5)
- {
- round++;
- check=SendCommand(refstream,"EHLO"+hostName,"EHLO","250");
- intround0=0;
- while(!check&&round0<5)
- {
- round0++;
- check=SendCommand(refstream,"EHLO"+hostName,"EHLO","250");
- }
- check=SendCommand(refstream,"AUTHLOGIN","AUTHLOGIN","334");
- check=SendCommand(refstream,Convert.ToBase64String(Encoding.Default.GetBytes(clsParam.Param.SendMailAccount)),"用户名","334");
- check=SendCommand(refstream,Convert.ToBase64String(Encoding.Default.GetBytes(clsParam.Param.SendMailPWD)),"密码","235");
- }
- if(!check)
- {
- thrownewException("邮件帐户身份验证失败!");
- }
- #endregion
- #region发件人
- check=SendCommand(refstream,"MAILFROM:<"+clsParam.Param.SendMailAccount+">","MAILFROM","250");
- round=0;
- while(!check&&round<5)
- {
- round++;
- check=SendCommand(refstream,"MAILFROM:<"+clsParam.Param.SendMailAccount+">","MAILFROM","250");
- }
- #endregion
- #region收件人
- check=SendCommand(refstream,"RCPTTO:<"+mailto[0]+">","RCPTTO","250");
- round=0;
- while(!check&&round<5)
- {
- round++;
- check=SendCommand(refstream,"RCPTTO:<"+mailto[0]+">","RCPTTO","250");
- }
- #endregion
- #region抄送人
- if(mailto.Length>1)
- {
- for(inti=1;i<mailto.Length;i++)
- {
- check=SendCommand(refstream,"RCPTTO:<"+mailto[i]+">","RCPTTO","250");
- round=0;
- while(!check&&round<5)
- {
- round++;
- check=SendCommand(refstream,"RCPTTO:<"+mailto[i]+">","RCPTTO","250");
- }
- }
- }
- #endregion
- #region密送人
- //这里看大家是否需要了,可以偷偷给自己发一份,呵呵
- #endregion
- #region请求发送邮件体
- check=SendCommand(refstream,"DATA","DATA","354");
- round=0;
- while(!check&&round<5)
- {
- round++;
- check=SendCommand(refstream,"DATA","DATA","354");
- }
- #endregion
- #region发送邮件头
- StringBuildermailhead=newStringBuilder();
- mailhead.Append("Subject:"+subject)
- .Append("/nDate:"+DateTime.Now.ToString("yyyy-MM-ddHH:mm:ss.fff"))
- .Append("/nFrom:"+"上报软件<"+clsParam.Param.SendMailAccount+">")
- .Append("/nTo:"+mailto[0]);
- if(mailto.Length>1)
- {
- mailhead.Append("/nCc:");
- for(inti=1;i<mailto.Length;i++)
- {
- mailhead.Append(mailto[i]+";");
- }
- }
- mailhead.Append("/nBcc:a@b.com/n/n"); //这个地方大家随便改,注意这只是头,而不是真正的接收者,客户端只会解释,而不会发送,发送的是完全由RCPT TO控制的
- #endregion
- #region发送邮件内容
- check=SendCommand(refstream,mailhead.ToString()
- +msg
- +"/n/r/n./r/n","信已发出,服务器","250",false);
- round=0;
- while(!check&&round<5)
- {
- round++;
- check=SendCommand(refstream,mailhead.ToString()
- +msg
- +"/n/r/n./r/n","信已发出,服务器","250",false);
- }
- #endregion
- }
- privateboolSendCommand(refNetworkStreamnetstream,stringcontent,stringrehead,stringreflag)
- {
- returnSendCommand(refnetstream,content,rehead,reflag,true);
- }
- privateboolSendCommand(refNetworkStreamnetstream,stringcontent,stringrehead,stringreflag,boolisNewLine)
- {
- boolretBool=false;
- try
- {
- WriteToNetStream(refnetstream,content,isNewLine);
- stringcome=ReadFromNetStream(refnetstream);
- AddLog(rehead+"应答:"+come+"/r/n");
- retBool=CheckForError(come,reflag);
- }
- catch
- {
- retBool=false;
- }
- returnretBool;
- }
- privatevoidWriteToNetStream(refNetworkStreamNetStream,stringCommand)
- {
- WriteToNetStream(refNetStream,Command,true);
- }
- privatevoidWriteToNetStream(refNetworkStreamNetStream,stringmessage,boolisNewLine)
- {
- stringstringToSend=isNewLine?message+"/r/n":message;
- byte[]arrayToSend=Encoding.Default.GetBytes(stringToSend.ToCharArray());
- NetStream.Write(arrayToSend,0,arrayToSend.Length);
- }
- privatestringReadFromNetStream(refNetworkStreamNetStream)
- {
- byte[]temp=newbyte[512];
- NetStream.Read(temp,0,temp.Length);
- returnEncoding.Default.GetString(temp);;
- }
- privateboolCheckForError(stringstrMessage,stringcheck)
- {
- if(strMessage.IndexOf(check)==-1)
- {
- returnfalse;
- }
- else
- {
- returntrue;
- }
- }
3.再包一层方法
- privateboolSendMail(stringsubject,stringmsg,stringattachpath)
- {
- boolretbool=false;
- try
- {
- this.btnSendTestMail.Enabled=false;
- string[]mails=this.txtNoticeMailAddress.Text.Split(";".ToCharArray(),StringSplitOptions.RemoveEmptyEntries);//收件人用;隔开
- if(clsParam.Param.IsProxy)
- {
- switch(clsParam.Param.ProxyType)
- {
- caseMailProxyType.HTTP:
- #regionHTTP方式处理
- if(sendTcp==null)
- {
- stringauthstr=clsParam.Param.ProxyUID+":"+clsParam.Param.ProxyPWD;
- stringauthproxy="CONNECT"+clsParam.Param.SendMailSmtpAdd+":"+clsParam.Param.SendMailSmtpPort
- +"HTTP/1.0/r/nProxy-Authorization:Basic"
- +Convert.ToBase64String(Encoding.Default.GetBytes(authstr))+"/r/n/r/n";//代理服务器审核
- booltestproxyflag=true;//测试时候使用,设置成false可以不使用代理的Socket方式发邮件
- if(testproxyflag)
- {
- sendTcp=newTcpClient(clsParam.Param.ProxyIP,clsParam.Param.ProxyPort);
- }
- else
- {
- sendTcp=newTcpClient(clsParam.Param.SendMailSmtpAdd,clsParam.Param.SendMailSmtpPort);//注释1
- }
- NetworkStreamstream=sendTcp.GetStream();
- //发送代理验证
- if(testproxyflag)
- {
- WriteToNetStream(refstream,authproxy,false);//注释2
- }
- //获取验证反馈
- stringresponse=ReadFromNetStream(refstream);
- AddLog("连接应答:"+response+"/r/n");
- boolcheck=false;
- if(testproxyflag)
- {
- check=CheckForError(response,"HTTP/1.0200")||
- CheckForError(response,"HTTP/1.1200");
- if(check)
- {
- AddLog("邮件代理连接成功/r/n");
- }
- else
- {
- thrownewException("邮件代理连接失败/r/n");
- }
- stringreceive=ReadFromNetStream(refstream);//这个一定要有,自己在这里走了点弯路
- AddLog("远端服务器连接应答:"+receive+"/r/n");
- check=CheckForError(receive,"220");
- if(check)
- {
- AddLog("远端服务器连接成功/r/n");
- }
- else
- {
- thrownewException("远端服务器连接失败/r/n");
- }
- }
- else
- {
- check=CheckForError(response,"220");//注释3
- if(check)
- {
- AddLog("邮件服务器连接成功/r/n");
- }
- else
- {
- thrownewException("邮件服务器连接失败/r/n");
- }
- }
- }
- try
- {
- //发送邮件
- MailSocketAlternation(mails,subject,msg,attachpath);
- }
- catch(Exceptionex)
- {
- sendTcp.Close();
- sendTcp=null;
- throwex;
- }
- finally
- {
- if(sendTcp!=null)
- {
- try
- {
- NetworkStreamstream=sendTcp.GetStream();
- WriteToNetStream(refstream,"QUIT");
- AddLog("QUIT应答:"+ReadFromNetStream(refstream)+"/r/n");
- }
- catch(Exceptionex)
- {
- throwex;
- }
- finally
- {
- sendTcp.Close();
- sendTcp=null;
- }
- }
- }
- #endregion
- break;
- caseMailProxyType.SOCKS4: //此种方式我软件没让他支持,如果大家想用可以使用CDO的方法试一下,我也没有试是否可行,但是Socket的方式肯定是可以的,只是要有一些变化,如果有人感兴趣可以给我留言。使用CDO需要在项目里引用Microsoft CDO for Windows 2000 Library和Microsoft ActiveX Data Objects 2.8 Library两个COM
- #regionSOCKS4方式处理--暂时不予支持
- //CDO.MessageoMsg=newCDO.Message();
- //oMsg.From=clsParam.Param.SendMailAccount;
- //oMsg.To=mails[0];
- //oMsg.Subject=subject;
- //oMsg.HTMLBody=msg;
- //if(File.Exists(attachpath))
- //{
- //oMsg.AddAttachment(attachpath,"","");
- //}
- //CDO.IConfigurationiConfg=oMsg.Configuration;
- //ADODB.FieldsoFields=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.
- Theconfigurationsendusername/sendpasswordorpostusername/postpasswordfieldsareusedtospecifycredentials.)
- Value=2代表NTLM验证方式(SecurePasswordAuthenticationinMicrosoftOutlookExpress)
- //oFields["http://schemas.microsoft.com/cdo/configuration/smtpserver"].Value=clsParam.Param.SendMailSmtpAdd;
- //oFields["http://schemas.microsoft.com/cdo/configuration/sendemailaddress"].Value=clsParam.Param.SendMailAccount;//sendermail
- //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;
- caseMailProxyType.SOCKS5: //同Socket4的注释
- #regionSOCKS5方式处理--暂时不予支持
- #endregion
- break;
- }
- }
- else
- { //这种方式我也列出来,方便大家参考,如果能直连外网,这个方式最简单
- #region标准方式发送邮件
- System.Net.Mail.MailMessagemail=newSystem.Net.Mail.MailMessage();
- SmtpClientSmtpServer=newSmtpClient();
- SmtpServer.Credentials=newSystem.Net.NetworkCredential(clsParam.Param.SendMailAccount,clsParam.Param.SendMailPWD);
- SmtpServer.Port=25;
- SmtpServer.Host=clsParam.Param.SendMailSmtpAdd;
- SmtpServer.EnableSsl=false;
- mail.From=newMailAddress(clsParam.Param.SendMailAccount,"上报软件",System.Text.Encoding.Default);
- mail.To.Add(mails[0]);
- if(mails.Length>1)
- {
- for(inti=1;i<mails.Length;i++)
- {
- mail.CC.Add(mails[i]);
- }
- }
- mail.Bcc.Add("a@b.com");
- if(File.Exists(attachpath))
- {
- mail.Attachments.Add(newAttachment(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(Exceptionex)
- {
- AddLog("发送邮件异常:"+ex.Message);
- retbool=false;
- }
- finally
- {
- this.btnSendTestMail.Enabled=true;
- }
- returnretbool;
- }
4.下面我们测试一下
- privatevoidbtnSendTestMail_Click(objectsender,EventArgse)
- {
- 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们有所帮助,有问题大家留言探讨。