2023年2月

我在随笔

Winform开发框架之框架演化
》和《
Winform开发框架之混合型框架的实现
》都对Winform框架的变种,混合型框架进行了比较详细的介绍,本文继续上篇对混合型框架进行进一步的说明。

1、框架的扇出介绍

混合型框架为了支持WCF方式和传统访问数据库方式两种对数据操作的方式,有两个地方有扇出操作,一个是在界面上调用接口对象获取数据的时候有扇出操作,为了实现WCF方式和传统访问数据库方式的处理,如下所示。当然,如果必要,也可以扩展成支持更多的类型,如可能存在旧系统的WebService调用方式等。

另外,整个框架支持Oracle、SqlServer、MySql、Sqlite、Access等数据库的访问操作,因此在业务层调用具体数据访问类的时候,根据配置的不同,具体构造的数据库访问对象也不同,因此,这里也有一个扇出操作,而且扇出数量和支持的数据库一致,如下所示。

2、混合型框架的辅助性模块集成

混合型框架可以看成是Winform框架高级版本,除了它本身是一个完整的业务系统外,它外围的所有辅助性模块均
(如通用权限、通用字典、通用附件管理、通用人员管理。。。。)都
实现了这种混合型的框架,因此使用非常方便,整个框架如果简化来看,就是在原有的Winform界面层,用接口调用方式,避免和业务逻辑类的紧耦合关系。由于他是通过接口方式的调用方式,它本身又可以通过配置指定指向WCF的实现,因此也囊括了WCF框架的一切特点。

3、混合型框架的项目及配置分离

说到WCF的访问方式,混合型框架把业务系统的WCF服务和辅助性公用模块的WCF服务分开,首先是服务分开,然后是客户端配置文件分开。

客户端配置文件分开,是通过把他们的服务配置信息分别用不同的文件表示,如辅助性模块的WCF配置文件为BaseWcfConfig.config,业务系统的WCF配置文件为WcfConfig.config,通过这样的分离设置,我们在主配置文件app.Config文件里面,就清爽很多了,如下所示。

  <appSettings>
    <!--软件名称-->
    <addkey="ApplicationName"value="深田之星仓库管理系统"/>
    <!--开发商名称-->
    <addkey="Manufacturer"value=""/>
    <!--组件的数据库类型:access、sqlserver、sqlite、oracle等,默认为sqlserver可不写-->
    <addkey="ComponentDbType"value="sqlserver"/>
    <addkey="AttachmentLicense"value="397cV0hDLlNlY3VybXR5fOS8jeWNjuiBqnzlua-lt57niLHlkK-o_6rmioDmnK-mnInpmZDlhbzlj7h8RmFsc2Uv" />
    
    <!--组件模块调用方式,采用WCF方式Value为wcf,采用Winform方式Value为win-->
    <addkey="CallerType"value="wcf"/>
    <!--WCF服务的配置文件地址-->
    <addkey="BaseWcfConfig"value="BaseWcfConfig.config"/>
    <addkey="WcfConfig"value="WcfConfig.config"/>
  </appSettings>

这样我们通过修改
CallerType
的内容(WCF或者WIN),就可以实现两种不同方式的访问了。

整个项目工程的布局,除了刚才介绍的WCF服务模块,其实还有很多其他模块的,如下面图所示,包括业务逻辑模块,服务接口调用模块,Winform界面模块、WCF服务逻辑模块,整个系统的模块就包含这些,当然外围的辅助性模块,如字典、权限、人员等等,他们各自按照这个方式进行组织处理,系统调用的时候,不会具体关心它们的调用逻辑,因为它们的调用方式已经通过约定的
CallerType
的内容进行了指定。

4、混合型框架的调用介绍

对于混合型框架,不管它的数据调用逻辑是按照传统的Winform方式,还是分布式的WCF服务调用方式,实现代码都是一样的,因为它是基于接口层调用,如下所示是在框架系统中调用数据字典模块获取数据的代码实现。

        /// <summary>
        ///根据字典类型获取对应的CListItem集合/// </summary>
        /// <param name="dictTypeName"></param>
        /// <returns></returns>
        public static CListItem[] GetDictByDictType(stringdictTypeName)
{
List
<CListItem> itemList = new List<CListItem>();
Dictionary
<string, string> dict = WHC.Dictionary.UI.CallerFactory<WHC.Dictionary.Facade.IDictDataService>.Instance.GetDictByDictType(dictTypeName);foreach (string key indict.Keys)
{
itemList.Add(
newCListItem(key, dict[key]));
}
returnitemList.ToArray();
}

系统框架调用自己的接口获取数据,模式也和上面一样,与传统的Winform框架调用代码相比,并没有增加任何工作流,只是调用对象有点变化而已。

        private void winGridViewPager1_OnDeleteSelected(objectsender, EventArgs e)
{
if (MessageDxUtil.ShowYesNoAndTips("您确定删除选定的记录么?") ==DialogResult.No)
{
return;
}
int[] rowSelected = this.winGridViewPager1.GridView1.GetSelectedRows();foreach (int iRow inrowSelected)
{
string ID = this.winGridViewPager1.GridView1.GetRowCellDisplayText(iRow, "ID");
CallerFactory
<IItemDetailService>.Instance.Delete(ID);
}
BindData();
}

如果是传统的Winform框架,它的删除操作的核心调用代码是如下所示,是不是很相似的呢?

BLLFactory<ItemDetail>.Instance.Delete(ID);

5、混合式框架的代码生成工具支持

当然,虽然混合型框架比传统的Winform框架和WCF开发框架更为通用,不过由于它引入了多一层,而且为了实现更多模块的分离,增加了一些设计上的复杂性,整个项目工程看起来显得复杂了一点,如下面就是一个以字典模块为例的混合型框架的内部结构。

为了实现更简单化的开发,更快更高效的完成混合型框架的开发工作,我扩展了我的代码生成工具Database2Sharp,使其支持这种混合型框架的代码生成工作,这样开发混合型框架就和开发其他两种Winform开发框架、WCF开发框架一样,非常方便了。

生成混合型框架项目的步骤就是在【EnterpriseLibrary代码生成】的最后一步进行勾选设置即可。

代码生成工具,生成整体性的混合型框架项目如下所示,只是没有下图的界面部分,这部分在实际开发过程中,结合我的混合型框架案例进行整合即可,另外也可以界使用Database2Sharp进行Winform界面的开发,这样整体性就非常方便操作了:

6、混合型框架的优化总结

虽然整体性的混合型框架比其他两种框架模块,总体增加了一些难度及复杂性,不过,为了使得整个混合型框架开发和使用更加方便,我已经在设计上做了很多相关的工作,力求更好、更高效的使用好这种混合型框架,下面是我对整体性的框架做了的优化改进工作。

1)把所有通用的模块开发好,方便更好的集成使用,更加高效利用通用模块,重复利用度更高;

2)把WCF服务发布和服务逻辑分开,更好管理和发布WCF服务,服务发布只需要svc文件,不含任何后台代码;

3)统一的业务调用规则和命名规则,所有模块的接口调用统一为CallerFactory<I***Service>方式,通用模块和框架的命名规则和机制完全一样。

4)WCF服务配置文件分离,通用性的辅助模块的配置文件为BaseWcfConfig.config,业务系统的WCF配置文件为WcfConfig.config,配置文件分离更方便管理和维护,减少主配置文件app.Config的复杂性。

5)最后一条,也是最重要的一条,就是代码生成工具Database2Sharp的同步支持。通过代码生成工具,更好、更快的生成整个混合性框架的代码和项目工程,一键解决所有的烦恼。Winform界面,利用代码生成工具Database2Sharp进行生成,然后在项目中整合即可。

本人一直致力于Winform开发框架的研究和使用,有时候项目要求是传统Winform的,直接访问数据库进行获取数据;有的时候项目要求WCF框架,以分布式访问WCF服务获取数据的,虽然在Winform和WCF框架,都有特定的应用场景和开发思路,但是希望把这两种传统式的框架,加以抽象和提炼,形成目前所介绍的混合型框架,这样所有完成混合型框架改造的通用模块(或一个小业务模块),都能无缝式的实现集成使用。这种混合型框架的特点和优点都非常明显,为我最终实现,一个框架,多个应用的开发平台集成方案迈出坚实的一步。

1、混合型框架的应用前景

当然,本篇文章不是对理论的抽象说明,这一向不是我的风格,前面我已经
在随笔

Winform开发框架之框架演化
》、《
Winform开发框架之混合型框架的实现
》、《
Winform开发框架之混合型框架的剖析
》都对Winform框架的变种,混合型框架进行了比较详细的介绍,我希望通过实实在在的项目展示,介绍这种框架的优点特点,以及相关的设计理念。这种框架虽然比较通用,但相对于传统Winform框架和传统WCF开发框架,都增加了不少项目模块,也同时增加了一定的复杂度,如果纯手工来建立这种项目,会有比较麻烦的痛苦经历。

因此,本篇文件就是介绍了如何使用代码生成工具Database2Sharp来生成一个混合型框架的项目,由于我的Database2Sharp能够获取所有数据库的元数据,因此能够较好实现各种项目架构的生成操作,这种结合代码工具生成项目的方式,也是我们提高效率,统一代码风格
,减少代码出错,提高代码健壮性
,分享框架经验的最佳方式。

混合型框架可以看成是Winform框架高级版本,可以很好用来实现一个框架,多个应用的开发平台,我们可以在主体业务框架上,根据需要扩展很多应用,如通用模块有通用权限、通用字典、通用附件管理、通用人员管理。。。。,如果基于业务集成,那么可以把很多小业务也通过这种方式开发,然后通过即插即用的方式实现业务定制化的使用,这些应用都实现了这种混合型的框架,因此集成使用会非常方便。

2、代码生成工具生成混合型框架

混合型框架一般包括业务逻辑模块(包括BLL、IDAL、DAL、Entity层),服务接口调用模块(用来整合Winform和WCF接口调用)
,WCF服务逻辑模块、WCF服务发布模块
,Winform界面模块共5个项目的分层,如下所示。

虽然是需要生成有5个项目的代码,而且每个项目之间还有一些特殊的引用以及各种命名规则,但是这些对于代码生成工具来说,都是很容易做到的事情。

代码生成工具,是结合数据库信息进行代码生成的过程,因此需要先获取对应数据库信息。

第一步,在Database2Sharp左边的树形数据库列表中,单击展开对应数据库的详细表,由于Database2Sharp是一次性加载方式,加载数据库表的信息同时,也会加载相关的视图、存储过程(Oracle、SqlServer)、以及表字段和表之间的关系等等信息。单击其中一个表,我们可以看到对应该表的字段信息,以及创建表的SQL语句,也就是我们常说的DDL脚本。

第二步,在数据库节点上,单击鼠标右键,选择【代码生成】- 【EnterpriseLibrary代码生成】菜单,开始生成代码如下所示。

第三步,选择相应的数据库以及数据库表,用于代码的生成。

第四步,设置相关的代码生成配置信息,包括项目主命名空间,输出目录,以及过滤表名前缀等。

第五步,确认代码生成操作,一般确认,就会对选定的表进行项目代码的生成操作,生成一体化的整体性解决方案。

如果项目是要生成基于WCF开发框架的,那么勾选“同时生成WCF服务项目”选项即可,勾选后,可以修改WCF服务的项目名称,方便创建不同的WCF业务项目。WCF项目是基于VS2010开发环境、.NET4.0框架的项目。

如果项目是生成基于混合型开发框架的,那么勾选“生成混合式框架项目”选项即可,勾选后,“同时生成WCF服务项目”也会默认勾选,因为混合型框架式基于Winform和WCF两种的混合体项目。WCF项目是基于VS2010开发环境、.NET4.0框架的项目,这是目前最优的配置信息了。

如果勾选生成后打开解决方案,那么代码生成完成后,就会自动打开项目工程,你可以尝试编译,一般情况下,可以完全编译通过的。

项目生成后,就生成了几个典型的项目分层结构,如图所示。

贴上几个详细的项目文件结构图,以便更好了解整体的项目结构。




代码生成工具的下载地址:
http://www.iqidi.com/Database2Sharp.htm

详细的代码生成工具Database2Sharp的介绍文档:
Database2Sharp代码生成工具-使用帮助.pdf

我在较早时期的一篇文章《
基于Lumisoft.NET实现的邮件发送功能
》有大致对这个Lumisoft.NET组件的使用进行了介绍,在《
DevExpress控件使用之RichEditControl的使用
》则对使用.NET API进行邮件发送进行了说明,其实,实现邮件发送,这两种方式是比较常见的,当然Lumisoft.NET组件除了提供邮件发送功能外,还提供了邮件接收等功能的处理(包括基于POP3协议和IMAP协议),而.NET则除了提供SMTP协议功能外,则没有提供POP3协议处理的相关类库,因此收取邮件这需要自己进行封装(需要也可以参考codeproject.com上的相关文章)。

1、.NET的邮件发送功能实现

.NET本身封装了一个SmtpClient类以及相关的邮件对象类,这样利用这些类库,也可以方便实现邮件的发送功能的了。

如添加发送人地址,抄送地址,以及暗送地址(多个地址用逗号分开)代码如下。

            string toEmails =mailInfo.ToEmail;string bcc = "";
mailInfo.RecipientBCC.ForEach(obj
=> bcc += string.Format("{0},", obj));
bcc
= bcc.Trim(',');string cc = "";
mailInfo.RecipientCC.ForEach(obj
=> cc += string.Format("{0},", obj));
cc
= cc.Trim(',');

MailMessage mail
= newMailMessage(settingInfo.MailFrom, toEmails);if (!string.IsNullOrEmpty(bcc))
{
mail.Bcc.Add(bcc);
}
if (!string.IsNullOrEmpty(cc))
{
mail.CC.Add(cc);
}

.NET的附件和嵌入式资源由对象Attachment和LinkedResource进行管理,他们的利用代码如下所示:

            //附件
            foreach (string fileName inmailInfo.Attachments)
{
mail.Attachments.Add(
newAttachment(fileName));
}
//嵌入资源 AlternateView view =AlternateView.CreateAlternateViewFromString(mailInfo.Body, Encoding.UTF8, MediaTypeNames.Text.Html);foreach (LinkedAttachementInfo link inmailInfo.EmbedObjects)
{
LinkedResource resource
= newLinkedResource(link.Stream, link.MimeType);
resource.ContentId
=link.ContentId;
view.LinkedResources.Add(resource);
}
mail.AlternateViews.Add(view);

发送邮件的其他部分代码如下所示

            mail.IsBodyHtml =mailInfo.IsBodyHtml;
mail.BodyEncoding
=Encoding.UTF8;
mail.Subject
=mailInfo.Subject;
mail.SubjectEncoding
=Encoding.UTF8;//发送账户设置信息 SmtpClient client = newSmtpClient();
client.Host
=settingInfo.SmtpServer;
client.Port
=settingInfo.SmptPort;
client.UseDefaultCredentials
= false;
client.DeliveryMethod
=SmtpDeliveryMethod.Network;
client.Credentials
= newNetworkCredential(settingInfo.SmtpUser, settingInfo.SmtpPass);bool success = false;try{
client.Send(mail);
success
= true;
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
//throw; }

上面利用.net的SmtpClient发送邮件操作的完整代码如下:

        /// <summary>
        ///发送外部邮件(系统配置,系统邮件)/// </summary>
        /// <param name="mailInfo">发送邮件信息</param>
        /// <returns></returns>
        publicCommonResult Send(MailInfo mailInfo)
{
CommonResult result
= newCommonResult();try{
AppConfig config
= newAppConfig();string MailDomain = config.AppConfigGet("MailDomain");string MailUsername = config.AppConfigGet("MailUsername");string MailPassword = config.AppConfigGet("MailPassword");string MailPort = config.AppConfigGet("MailPort");string MailFrom = config.AppConfigGet("MailFrom");int port = 25;int.TryParse(MailPort, outport);

SmtpSettingInfo settingInfo
= newSmtpSettingInfo(MailDomain, port,
MailUsername, MailPassword, MailFrom);

result.Success
=PrivateSendEmail(mailInfo, settingInfo);
}
catch(Exception ex)
{
result.ErrorMessage
=ex.Message;throw;
}
returnresult;
}
/// <summary> ///通用发送邮件操作/// </summary> private static boolPrivateSendEmail(MailInfo mailInfo, SmtpSettingInfo settingInfo)
{
string toEmails =mailInfo.ToEmail;string bcc = "";
mailInfo.RecipientBCC.ForEach(obj
=> bcc += string.Format("{0},", obj));
bcc
= bcc.Trim(',');string cc = "";
mailInfo.RecipientCC.ForEach(obj
=> cc += string.Format("{0},", obj));
cc
= cc.Trim(',');

MailMessage mail
= newMailMessage(settingInfo.MailFrom, toEmails);if (!string.IsNullOrEmpty(bcc))
{
mail.Bcc.Add(bcc);
}
if (!string.IsNullOrEmpty(cc))
{
mail.CC.Add(cc);
}
//附件 foreach (string fileName inmailInfo.Attachments)
{
mail.Attachments.Add(
newAttachment(fileName));
}
//嵌入资源 AlternateView view =AlternateView.CreateAlternateViewFromString(mailInfo.Body, Encoding.UTF8, MediaTypeNames.Text.Html);foreach (LinkedAttachementInfo link inmailInfo.EmbedObjects)
{
LinkedResource resource
= newLinkedResource(link.Stream, link.MimeType);
resource.ContentId
=link.ContentId;
view.LinkedResources.Add(resource);
}
mail.AlternateViews.Add(view);
mail.IsBodyHtml
=mailInfo.IsBodyHtml;
mail.BodyEncoding
=Encoding.UTF8;
mail.Subject
=mailInfo.Subject;
mail.SubjectEncoding
=Encoding.UTF8;//发送账户设置信息 SmtpClient client = newSmtpClient();
client.Host
=settingInfo.SmtpServer;
client.Port
=settingInfo.SmptPort;
client.UseDefaultCredentials
= false;
client.DeliveryMethod
=SmtpDeliveryMethod.Network;
client.Credentials
= newNetworkCredential(settingInfo.SmtpUser, settingInfo.SmtpPass);bool success = false;try{
client.Send(mail);
success
= true;
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
//throw; }string message = string.Format("发送给【{0}】的邮件“{1}”,{2},时间:{3}",
mailInfo.ToEmail[
0], mailInfo.Subject, success ? "发送成功" : "发送失败", DateTime.Now);
LogTextHelper.Info(message);
returnsuccess;
}

2、基于Lumisoft.NET组件的邮件发送功能实现

基于Lumisoft.NET组件的邮件发送,也是一种很常用的,因为这个开源组件非常强大,经常可以在一些程序中被使用。

这个发送邮件的功能主要是利用SMTP_Client类来实现的,如下代码所示。注意其中的Authenticate函数已经被舍弃,可以使用Auth方法进行验证。但是函数参数有所不同,根据验证对象,使用不同的验证方式,一般选择AUTH_SASL_Client_Plain对象即可。

        public boolSend()
{
bool sended = false;using (SMTP_Client client = newSMTP_Client())
{
client.Connect(smtpServer, smtpPort, smtpUseSsl);
client.EhloHelo(smtpServer);
var authhh = newAUTH_SASL_Client_Plain(username, password);
client.Auth(authhh);
//client.Authenticate(username, password);//string text = client.GreetingText; client.MailFrom(from, -1);foreach (string address intoList.Keys)
{
client.RcptTo(address);
}
//采用Mail_Message类型的Stream Mail_Message m = Create_PlainText_Html_Attachment_Image(toList, ccList, from, fromDisplay, subject, body, attachments);using (MemoryStream stream = newMemoryStream())
{
m.ToStream(stream,
newMIME_Encoding_EncodedWord(MIME_EncodedWordEncoding.Q, Encoding.UTF8), Encoding.UTF8);
stream.Position
= 0;
client.SendMessage(stream);

sended
= true;
}
if (m != null)
{
m.Dispose();
}

client.Disconnect();
}
returnsended;
}

构造用于SMTP发送的数据,可以使用Mail_Message 对象,也可以使用Mime对象,虽然读都可以实现发送功能,不过Mime对象是舍弃的对象了。

构造Mail_Message对象后,创建用于发送的格式要转换为Stream对象。转换为发送的Stream操作如下所示。

using (MemoryStream stream = newMemoryStream())
{
m.ToStream(stream,
newMIME_Encoding_EncodedWord(MIME_EncodedWordEncoding.Q, Encoding.UTF8), Encoding.UTF8);
stream.Position
= 0;
client.SendMessage(stream);

sended
= true;
}

构造Mail_Message格式的邮件操作如下所示。

        private Mail_Message Create_PlainText_Html_Attachment_Image(Dictionary<string,string> tomails, Dictionary<string, string> ccmails, string mailFrom, stringmailFromDisplay,string subject, string body, Dictionary<string, string> attachments, string notifyEmail = "", string plaintTextTips = "")
{
Mail_Message msg
= newMail_Message();
msg.MimeVersion
= "1.0";
msg.MessageID
=MIME_Utils.CreateMessageID();
msg.Date
=DateTime.Now;
msg.Subject
=subject;
msg.From
= newMail_t_MailboxList();
msg.From.Add(
newMail_t_Mailbox(mailFromDisplay, mailFrom));
msg.To
= newMail_t_AddressList();foreach (string address intomails.Keys)
{
string displayName =tomails[address];
msg.To.Add(
newMail_t_Mailbox(displayName, address));
}
msg.Cc
= newMail_t_AddressList();foreach (string address inccmails.Keys)
{
string displayName =ccmails[address];
msg.Cc.Add(
newMail_t_Mailbox(displayName, address));
}
//设置回执通知 if (!string.IsNullOrEmpty(notifyEmail) &&ValidateUtil.IsEmail(notifyEmail))
{
msg.DispositionNotificationTo.Add(
newMail_t_Mailbox(notifyEmail, notifyEmail));
}
#region MyRegion //--- multipart/mixed ----------------------------------- MIME_h_ContentType contentType_multipartMixed = newMIME_h_ContentType(MIME_MediaTypes.Multipart.mixed);
contentType_multipartMixed.Param_Boundary
= Guid.NewGuid().ToString().Replace('-', '.');
MIME_b_MultipartMixed multipartMixed
= newMIME_b_MultipartMixed(contentType_multipartMixed);
msg.Body
=multipartMixed;//--- multipart/alternative ----------------------------- MIME_Entity entity_multipartAlternative = newMIME_Entity();
MIME_h_ContentType contentType_multipartAlternative
= newMIME_h_ContentType(MIME_MediaTypes.Multipart.alternative);
contentType_multipartAlternative.Param_Boundary
= Guid.NewGuid().ToString().Replace('-', '.');
MIME_b_MultipartAlternative multipartAlternative
= newMIME_b_MultipartAlternative(contentType_multipartAlternative);
entity_multipartAlternative.Body
=multipartAlternative;
multipartMixed.BodyParts.Add(entity_multipartAlternative);
//--- text/plain ---------------------------------------- MIME_Entity entity_text_plain = newMIME_Entity();
MIME_b_Text text_plain
= newMIME_b_Text(MIME_MediaTypes.Text.plain);
entity_text_plain.Body
=text_plain;//普通文本邮件内容,如果对方的收件客户端不支持HTML,这是必需的 string plainTextBody = "如果你邮件客户端不支持HTML格式,或者你切换到“普通文本”视图,将看到此内容";if (!string.IsNullOrEmpty(plaintTextTips))
{
plainTextBody
=plaintTextTips;
}

text_plain.SetText(MIME_TransferEncodings.QuotedPrintable, Encoding.UTF8, plainTextBody);
multipartAlternative.BodyParts.Add(entity_text_plain);
//--- text/html ----------------------------------------- string htmlText = body;//"<html>这是一份测试邮件,<img src=\"cid:test.jpg\">来自<font color=red><b>LumiSoft.Net</b></font></html>"; MIME_Entity entity_text_html = newMIME_Entity();
MIME_b_Text text_html
= newMIME_b_Text(MIME_MediaTypes.Text.html);
entity_text_html.Body
=text_html;
text_html.SetText(MIME_TransferEncodings.QuotedPrintable, Encoding.UTF8, htmlText);
multipartAlternative.BodyParts.Add(entity_text_html);
//--- application/octet-stream ------------------------- WebClient client = newWebClient();foreach (string attach inattachments.Keys)
{
try{byte[] bytes =client.DownloadData(attach);using (MemoryStream stream = newMemoryStream(bytes))
{
multipartMixed.BodyParts.Add(Mail_Message.CreateAttachment(stream, attachments[attach]));
}
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
}
}
#endregion returnmsg;
}

而构造Mime格式的操作如下所示。

        private Mime Create_Html_Attachment_Image(string mailTo, string mailFrom, stringmailFromDisplay,string subject, string body, List<string> attachments, Dictionary<string, string> embedImages, string notifyEmail = "", string plaintTextTips = "",string replyEmail = "")
{
Mime m
= newMime();
MimeEntity mainEntity
=m.MainEntity;

mainEntity.From
= newAddressList();
mainEntity.From.Add(
newMailboxAddress(mailFromDisplay, mailFrom));
mainEntity.To
= newAddressList();
mainEntity.To.Add(
newMailboxAddress(mailTo, mailTo));
mainEntity.Subject
=subject;
mainEntity.ContentType
=MediaType_enum.Multipart_mixed;//设置回执通知 if (!string.IsNullOrEmpty(notifyEmail) &&ValidateUtil.IsEmail(notifyEmail))
{
mainEntity.DSN
=notifyEmail;
}
//设置统一回复地址 if (!string.IsNullOrEmpty(replyEmail) &&ValidateUtil.IsEmail(replyEmail))
{
mainEntity.ReplyTo
= newAddressList();
mainEntity.ReplyTo.Add(
newMailboxAddress(replyEmail, replyEmail));
}

MimeEntity textEntity
=mainEntity.ChildEntities.Add();
textEntity.ContentType
=MediaType_enum.Text_html;
textEntity.ContentTransferEncoding
=ContentTransferEncoding_enum.QuotedPrintable;
textEntity.DataText
=body;//附件 foreach (string attach inattachments)
{
MimeEntity attachmentEntity
=mainEntity.ChildEntities.Add();
attachmentEntity.ContentType
=MediaType_enum.Application_octet_stream;
attachmentEntity.ContentDisposition
=ContentDisposition_enum.Attachment;
attachmentEntity.ContentTransferEncoding
=ContentTransferEncoding_enum.Base64;
FileInfo file
= newFileInfo(attach);
attachmentEntity.ContentDisposition_FileName
=file.Name;
attachmentEntity.DataFromFile(attach);
}
//嵌入图片 foreach (string key inembedImages.Keys)
{
MimeEntity attachmentEntity
=mainEntity.ChildEntities.Add();
attachmentEntity.ContentType
=MediaType_enum.Application_octet_stream;
attachmentEntity.ContentDisposition
=ContentDisposition_enum.Inline;
attachmentEntity.ContentTransferEncoding
=ContentTransferEncoding_enum.Base64;string imageFile =embedImages[key];
FileInfo file
= newFileInfo(imageFile);
attachmentEntity.ContentDisposition_FileName
=file.Name;//string displayName = Path.GetFileNameWithoutExtension(fileName); attachmentEntity.ContentID = key;//BytesTools.BytesToHex(Encoding.Default.GetBytes(fileName)); attachmentEntity.DataFromFile(imageFile);
}
returnm;
}

综合以上两者的发送功能,都可以实现邮件的发送操作,如下界面是发送邮件界面。

3、LumiSoft.NET存储eml邮件文件以及发送eml文件操作

除了上面的发送普通邮件,Lumisoft还支持吧邮件序列号存储到文件(.eml邮件文件)里面,然后也可以通过把文件读取到流里面,进行发送,对于某种场合,可以把邮件存储到eml文件是一个很好的操作。

存储EML文件的相关操作如下所示。

        private void btnCreateFile_Click(objectsender, EventArgs e)
{
string attachFile = Path.Combine(Application.StartupPath, "Attachment/Hotel2.png");
List
<string> attachments = new List<string>();
attachments.Add(attachFile);
string subject = "测试邮件";string body = "<html>这是一份测试邮件,来自<font color=red><b>LumiSoft.Net</b></font></html>";string bodyEmbedy = "<html>这是一份测试邮件<img src=\"cid:test.jpg\">,来自<font color=red><b>LumiSoft.Net</b></font></html>";
Dictionary
<string, string> embedList = new Dictionary<string, string>();
embedList.Add(
"test.jpg", "C:\\test.jpg");//存储为Eml文件 string path = Path.Combine(Application.StartupPath, "Eml");
DirectoryUtil.AssertDirExist(path);
string emlFile = string.Format("{0}/{1}.eml", path, DateTime.Now.ToFileTime());

Mime m
= Create_Html_Attachment_Image(to, from, from, subject, bodyEmbedy, attachments, embedList);
m.ToFile(emlFile);

MessageUtil.ShowTips(
"OK");
}

发送EML文件操作如下所示。

        private void btnSendFile_Click(objectsender, EventArgs e)
{
using (SMTP_Client client = newSMTP_Client())
{
int smtpPort = smtpUseSsl ?WellKnownPorts.SMTP_SSL : WellKnownPorts.SMTP;

client.Connect(smtpServer, smtpPort, smtpUseSsl);
client.EhloHelo(smtpServer);
//var authhh = new AUTH_SASL_Client_Plain(username, password);//client.Auth(authhh); client.Authenticate(username, password);//string text = client.GreetingText; client.MailFrom(from, -1);
client.RcptTo(to);
string path = Path.Combine(Application.StartupPath, "Eml");string emlFile = Directory.GetFiles(path)[0];var msg =Mail_Message.ParseFromFile(emlFile);

MemoryStream stream
= newMemoryStream();
msg.ToStream(stream,
newMIME_Encoding_EncodedWord(MIME_EncodedWordEncoding.Q, Encoding.UTF8), Encoding.UTF8);
stream.Position
= 0;
client.SendMessage(stream);
client.Disconnect();
}
MessageUtil.ShowTips(
"OK");
}

Lumisoft.NET组件是一个非常强大的邮件发送、邮件接收等功能的开源组件,一般用它来处理邮件的相关操作,是非常合适的。之前也写过一些该组件的随笔文章,不过主要是利用来发送邮件居多,最近由于项目需要,需要利用该组件来接收邮件,邮件通过POP3协议进行接收到本地,故对该组件进行了全面的了解和使用。本文主要是在此背景上,介绍该组件的POP3协议处理类的使用。Lumisoft.NET组件2013年作者有做了一定的更新,修复了一些问题,本文是基于该组件的最新版本进行开发使用。

1、POP3登录及头部信息获取

首先使用POP3,必须创建一个
POP3_Client
的对象,然后通过Connect和Login进行连接和登录处理,相关的代码如下所示。

            using (POP3_Client popClient = newPOP3_Client())
{
popClient.Logger
= newLogger();
popClient.Logger.WriteLog
+= new EventHandler<WriteLogEventArgs>(WriteLog);

popClient.Connect(pop3Server, pop3Port, pop3UseSsl);
popClient.Login(username, password);

POP3的的邮件下载通过
POP3_Client
对象的属性
Messages对象进行,每个POP3_ClientMessage代表一份完整的邮件信息,一开始应该是只是获取一些简单的邮件信息(其中包括邮件的唯一标识UID),这样才能提高POP3协议的处理速度,如下代码所示。

foreach (POP3_ClientMessage message in popClient.Messages)

为了进一步获取邮件头部信息,那么需要进行下面的转换

Mail_Message mime_header = Mail_Message.ParseFromByte(message.HeaderToByte());

转换后Mail_Message承载了邮件头部文件的很多必备信息,如发送人,发送人名称,接收地址,抄送人地址,邮件标题,邮件日期等等信息。

这些邮件地址的信息,都是通过Mail_t_Mailbox对象来记录,一般包含邮件地址的Address和显示名称DisplayName,这样非常方便用来显示,如我们可以进行转义,记录到数据库里面。

                        if (mime_header.From != null)
{
//伍华聪(wuhuacong@163.com) string displayname = mime_header.From[0].DisplayName;string from = mime_header.From[0].Address;//DecodeString(mime_header.From[0].Address); if (!string.IsNullOrEmpty(displayname))
{
info.From
= string.Format("{0}({1})", displayname, from);
}
else{
info.From
= string.Format("{0}", from);
}
}
                        if (mime_header.To != null)
{
StringBuilder sb
= newStringBuilder();foreach (Mail_t_Mailbox recipient inmime_header.To.Mailboxes)
{
string displayname =recipient.DisplayName;string address =recipient.Address;if (!string.IsNullOrEmpty(displayname))
{
sb.AppendFormat(
"{0}({1});", displayname, address);
}
else{
sb.AppendFormat(
"{0};", address);
}
}
info.Senders
= sb.ToString().Trim(';');
}
if (mime_header.Cc != null)
{
StringBuilder sb
= newStringBuilder();foreach (Mail_t_Mailbox recipient inmime_header.Cc.Mailboxes)
{
string displayname =recipient.DisplayName;string address =recipient.Address;if (!string.IsNullOrEmpty(displayname))
{
sb.AppendFormat(
"{0}({1});", displayname, address);
}
else{
sb.AppendFormat(
"{0};", address);
}
}
info.Carboncopy
= sb.ToString().Trim(';');
}

每封Email会有一个在Pop3服务器范围内唯一的Id,检查这个Id是否存在就可以知道以前有没有接收过这封邮件

info.MailUid = message.UID;

每份邮件的头部信息,都会包含一个日期的,如下可以获取到该日期

info.Date = mime_header.Date;

标题信息可以通过下面代码获取

info.Title = mime_header.Subject;/

2、邮件正文信息和附件信息的获取

如果需要进一步获取邮件的正文内容,则需要对信息进行进一步的转换,把message对象进行MessageToByte操作,然后利用函数Mail_Message.ParseFromByte进行转换。

byte[] messageBytes =message.MessageToByte();

Mail_Message mime_message
=Mail_Message.ParseFromByte(messageBytes);if (mime_message == null) continue;
info.Body
=mime_message.BodyText;try{if (!string.IsNullOrEmpty(mime_message.BodyHtmlText))
{
info.Body
=mime_message.BodyHtmlText;
}
}
catch{//屏蔽编码出现错误的问题,错误在BodyText存在而BodyHtmlText不存在的时候,访问BodyHtmlText会出现 }

邮件的附件是通过MIME_Entity来承载信息的,所以我们需要把对象通过mime_message.GetAttachments(true, true)进行获取,转换为附件信息。

                        #region 邮件附件内容
                        foreach (MIME_Entity entity in mime_message.GetAttachments(true, true))
{
if (entity.ContentDisposition != null &&entity.ContentDisposition.Param_FileName!= null)
{
//Console.WriteLine("Attachment: " + entity.ContentDisposition.Param_FileName); string fileName = entity.ContentDisposition.Param_FileName;

如果需要进一步获取附件里面的文件字节流,那么还需要进行进一步的转换为MIME_b_SinglepartBase对象。

 MIME_b_SinglepartBase byteObj =(MIME_b_SinglepartBase)entity.Body;if (byteObj != null)
{
FileUtil.CreateFile(filePath, byteObj.Data);
fileSize
= byteObj.Data.Length;

如果要区分邮件里面的附件是内嵌图片附件还是真正的附件,那么可以通过下面代码进行判断,如果是MIME_DispositionTypes.Attachment的就是普通附件,MIME_DispositionTypes.Inline的就是内嵌正文的附件。

entity.ContentDisposition.DispositionType == MIME_DispositionTypes.Attachment

3、邮件的删除操作

服务器上的邮件,可以通过POP3的协议方式进行删除,删除操作很简单,主要是通过mail.MarkForDeletion进行标识即可,实例操作代码如下所示

            using (POP3_Client c = newPOP3_Client())
{
c.Connect(pop3Server, pop3Port, pop3UseSsl);
c.Login(username, password);
if (c.Messages.Count > 0)
{
foreach (POP3_ClientMessage mail inc.Messages)
{
try{if(toDeleteMailUidList.Contains(mail.UID))
{
mail.MarkForDeletion();

deletedList.Add(mail.UID);
}
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
}
}
}
}

在Lumisoft.NET组件获取POP3邮件的时候,发现大多数邮件都能正常获取,不过对于一些特殊的邮件,好像总是会出现转换错误,或者出现乱码及部分乱码现象,有些在标题里面或者邮件接收人地址,而有些则在内容里面,为了更好整理相关的问题,写了本文,希望对大家使用该组件有一定的帮助作用。

1、
日期转换出错问题。

错误信息:
[2013-05-04 10:49:03]    转换邮件的Date出错:账号wuhuacong@163.com 邮件标题:ICP???????????????????????wuhuacong)

LumiSoft.Net.ParseException: Header field 'Date' parsing failed.

在 LumiSoft.Net.Mail.Mail_Message.get_Date()

在 WHC.PlugInService.Pop3Helper.Receive() 位置 ......\Pop3Helper.cs:行号 160

错误原因:
由于邮件格式的日期内容格式不同,导致无法正常解析。如一般的格式为下面

Message-ID: <d74841c5887b4df692ebdb7ec7802054@4782e72954a24cc89535840ea2e5da5b>Date: Fri,26 Apr 2013 08:56:52GMT
Mime
-Version: 1.0From:"wuhuacong2013@163.com" <wuhuacong2013@163.com>To:"wuhuacong@96900.com.cn" <wuhuacong@96900.com.cn>

有些邮件日期格式是2013-05-06 19:01:44,则Lumisoft组件无法解析,需要跟踪到他的邮件日期处理的代码,然后进行修改才可以实现正常的邮件日期解析了。

官方的代码如下所示。

        publicDateTime Date
{
get{if(this.IsDisposed){throw new ObjectDisposedException(this.GetType().Name);
}

MIME_h h
= this.Header.GetFirst("Date");if(h != null){try{returnMIME_Utils.ParseRfc2822DateTime(((MIME_h_Unstructured)h).Value);
}
catch{throw new ParseException("Header field 'Date' parsing failed.");
}
}
else{returnDateTime.MinValue;
}
}
set{if(this.IsDisposed){throw new ObjectDisposedException(this.GetType().Name);
}
if(value ==DateTime.MinValue){this.Header.RemoveAll("Date");
}
else{
MIME_h h
= this.Header.GetFirst("Date");if(h == null){this.Header.Add(new MIME_h_Unstructured("Date",MIME_Utils.DateTimeToRfc2822(value)));
}
else{this.Header.ReplaceFirst(new MIME_h_Unstructured("Date",MIME_Utils.DateTimeToRfc2822(value)));
}
}
}
}

需要增加对普通日期格式的修改,修改后的代码如下所示

        publicDateTime Date
{
get{if(this.IsDisposed){throw new ObjectDisposedException(this.GetType().Name);
}

MIME_h h
= this.Header.GetFirst("Date");if(h != null){try{returnMIME_Utils.ParseRfc2822DateTime(((MIME_h_Unstructured)h).Value);
}
catch{//尝试转换正常的日期 DateTime dt;string dateString =((MIME_h_Unstructured)h).Value;bool success = DateTime.TryParse(dateString, outdt);if(success)
{
returndt;
}
else{throw new ParseException("Header field 'Date' parsing failed.");
}
}
}
else{returnDateTime.MinValue;
}
}
set{if(this.IsDisposed){throw new ObjectDisposedException(this.GetType().Name);
}
if(value ==DateTime.MinValue){this.Header.RemoveAll("Date");
}
else{
MIME_h h
= this.Header.GetFirst("Date");if(h == null){this.Header.Add(new MIME_h_Unstructured("Date",MIME_Utils.DateTimeToRfc2822(value)));
}
else{this.Header.ReplaceFirst(new MIME_h_Unstructured("Date",MIME_Utils.DateTimeToRfc2822(value)));
}
}
}
}

2、由于意外的数据包格式,握手失败

错误信息
:[2013-05-04 10:13:54]    System.IO.IOException: 由于意外的数据包格式,握手失败。

在 LumiSoft.Net.TCP.TCP_Client.Connect(String host, Int32 port, Boolean ssl)

在 WHC.PlugInService.SmtpHelper.Send() 位置 ........\SmtpHelper.cs:行号 123

在 WHC.PlugInService.SendMailService.DataThreadHandle(MailSendConfigInfo info) 位置 ...............\SendMailService.cs:行号 66

错误原因
:由于POP3的配置端口不正确导致,一般的端口必须严格按照正常的来填写。

邮件SMTP
和POP3
常用配置说明:

邮箱

Smtp服务器

Smtp端口

POP3服务器

POP3端口

使用SSL

Gmail.com

smtp.gmail.com

465

pop.gmail.com

995

true

QQ.com

smtp.qq.com

25

pop.qq.com

110

true

163.com

smtp.163.com

25

pop.163.com

110

false

Sina.com

smtp.sina.com

25

pop.sina.com

110

false

其他

smtp.test.com

25

pop.test.com

110

false

3、邮件标题乱码问题

错误信息
:

标题出现类似
=?utf-8?B?5rWL6K+V6YKu5Lu2?=

错误原因
:这个是因为编码的问题,其中=?utf-8?B是表示该段字符为UTF-8的格式,后面的是base64格式的内容。除了utf-8,还可以出现gb2312或者ibm-euccn等格式。为了转换上面的编码问题,我写了一个转码函数,如下所示。

        private string DecodeString(stringinput)
{
string regex = @"=\?(?<encode>.*?)\?B\?(?<body>.*?)\?=";

Regex re
= new Regex(regex, RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace |RegexOptions.Multiline);
MatchCollection mcs
=re.Matches(input);foreach (Match mc inmcs)
{
string encode = mc.Groups["encode"].Value;if (!string.IsNullOrEmpty(encode))
{
if (encode.ToLower().Contains("euccn") || encode.ToLower().Contains("euc-cn") ||encode.ToLower().Contains("gbk"))
{
encode
= "gb2312";
}
else if (encode.ToLower().Contains("utf8"))
{
encode
= "utf-8";
}
string body = mc.Groups["body"].Value;byte[] bytes =Convert.FromBase64String(body);string result =Encoding.GetEncoding(encode).GetString(bytes);

input
=input.Replace(mc.Value, result);
}
}
returninput;
}

如可以通过代码吧标题进行转码解析

info.Title = DecodeString(mime_header.Subject);

转码后,标题和相关的内容都可以正常显示了。

除了上面的转码操作,还有一种更好的方法,能够使得邮件相关信息正常显示。

因为通过分析了解到,由于Lumisoft的Mail_Message.ParseFromByte函数默认只是以UTF8转换字节,一旦字节为GB2312格式,就会发生转换乱码问题,因此先经过Default编码转换,然后再以UTF8获取字节,即可正常转换邮件头部。

byte[] utf8Bytes =Encoding.UTF8.GetBytes(message.HeaderToString());
Mail_Message mime_header
= Mail_Message.ParseFromByte(utf8Bytes);

这样获取到的标题,以及邮件头部等信息,都是正常的了。