using System;
using System.Collections;
using System.IO;
using System.Text;
using System.Xml;
using Bitworking.Smtp;
namespace Bitworking{
public class MailHeaderAndBody {
public string from;
public string subject;
public string to;
public StringBuilder body;
}
public class RssSmtpSink : IRssSink {
// These fields are not really instace fields.
// They are scratch-pad fields used to pass state
// between implementation functions to reduce argument count.
private string title_;
private string link_;
private string description_;
private string baseUrl_;
private string parserComments_;
// Default SMTP envelope
private string from_;
private string to_;
private string via_;
public void NewAggregatedXml(bool freshFeeds) {
// Note: Emailer doesn't send mail on read items,
// regardless of the state of the
// "show old items" checkbox.
// The following control is useful for debugging.
bool forceEmailGenerationForReadItems = false;
if (freshFeeds) {
SmtpConfigInfo config = new SmtpConfigInfo();
SerializeByAttributes.Read(config, "Aggie.xfg");
if (config.enableMailer) {
SetDefaultEnvelope( config );
SmtpBatchMailer batchMailer = new SmtpBatchMailer( config );
string itemSelectionPath =
(forceEmailGenerationForReadItems ? "item" : "item[@read='false']");
string channelSelectionPath = "channel";
string siteSelectionPath = "//aggregate/site";
XmlDocument doc = new XmlDocument();
doc.Load("aggregated.xml");
XmlNodeList sites = doc.SelectNodes(siteSelectionPath);
foreach (XmlNode site in sites) {
XmlNode channel = site.SelectSingleNode(channelSelectionPath);
if (null != channel) {
title_ = GetElementValue(channel, "title");
link_ = GetElementValue(channel, "link");
description_ = GetElementValue(channel, "description");
baseUrl_ = GetElementValue(channel, "base");
parserComments_ = GetElementValue(channel, "comments");
// TODO: There must be a nicer way to do this:
if ( parserComments_ != "" ) {
parserComments_ = parserComments_.Replace( "<strict>", "<p>[Strict parser] " );
parserComments_ = parserComments_.Replace( "</strict>", "</p>\r\n" );
parserComments_ = parserComments_.Replace( "<loose>", "<p>[Loose parser] " );
parserComments_ = parserComments_.Replace( "</loose>", "</p>\r\n" );
}
XmlNodeList items = site.SelectNodes(itemSelectionPath);
if ( null != items ){
NewAggregatedChannel( config, batchMailer, items );
}
}
}
batchMailer.Close();
}
}
} // NewAggregatedXml
private void NewAggregatedChannel(
SmtpConfigInfo config,
SmtpBatchMailer batchMailer,
XmlNodeList items ) {
// We have, say, M items in the channel, and need to group them into N
// groups. How this grouping is done is controlled by a config option:
// 1. Each item is in its own mail message (N=M)
// 2. All items are in a single mail message (N=1)
// 3. Items are grouped according to some criteria.
// Currently that criteria is the item title.
bool reverseItems = config.reversItems;
int howToGroup = config.howToGroup;
// Make a modifieable list of items
// TODO: (ZivC) Must be a better way to do this in .NET
ArrayList itemsList = new ArrayList();
foreach (XmlNode node in items) {
itemsList.Add( node );
}
// Reverse items if necessary
if ( reverseItems )
itemsList.Reverse();
MailHeaderAndBody msg;
switch ( howToGroup ) {
case 0: // N=M
foreach ( XmlNode node in itemsList ) {
ComposeMailFromItem( node, out msg );
MailItem( config, batchMailer, msg );
}
break;
case 1: // N=1
ComposeMailHeader( "", "", out msg );
foreach ( XmlNode node in itemsList ) {
ComposeMailItem( node, ref msg );
}
ComposeMailFooter( ref msg );
MailItem( config, batchMailer, msg );
break;
case 2: // Group by title
while ( itemsList.Count > 0 ) {
ArrayList group = new ArrayList();
group.Add( itemsList[0] );
itemsList.RemoveAt( 0 );
string title = GetElementValue((XmlNode)group[0], "title");
int other = 0;
while ( other < itemsList.Count ) {
string otherTitle = GetElementValue((XmlNode)itemsList[other], "title" );
if ( title == otherTitle ) {
group.Add( itemsList[other] );
itemsList.RemoveAt( other );
}
else
other++;
}
ComposeMailHeader( title, "", out msg );
foreach ( XmlNode node in group ) {
ComposeMailItem( node, ref msg );
}
ComposeMailFooter( ref msg );
MailItem( config, batchMailer, msg );
} // while more items to process
break;
} // switch
} // NewAggregatedChannel
private string GetElementValue(XmlNode node, string path) {
string returnValue = "";
XmlNode target = node.SelectSingleNode(path);
if (null != target) {
string encoded = target.InnerXml;
returnValue = System.Web.HttpUtility.HtmlDecode( encoded );
}
return returnValue;
} // GetElementValue
private void SetDefaultEnvelope( SmtpConfigInfo config ) {
string sendingAccount = StringUtils.StringOrFallback( config.sendingAccount, "" );
string receivingAccount = StringUtils.StringOrFallback( config.receivingAccount, sendingAccount );
// Wrap account names with a "<...>" envelope if required
if ( sendingAccount.IndexOf( "<" ) < 0 )
sendingAccount = "<" + sendingAccount + ">";
if ( receivingAccount.IndexOf( "<" ) < 0 )
receivingAccount = "<" + receivingAccount + ">";
from_ = "Aggie " + sendingAccount;
to_ = "Aggie " + receivingAccount;
via_ = " (via Aggie)\" " + sendingAccount;
} // SetDefaultEnvelope
private void ComposeMailHeader( string itemTitle, string itemLink, out MailHeaderAndBody msg ) {
msg = new MailHeaderAndBody();
// 'From' field
// TODO: I don't like this. What if title_ has a " ?
// I need to think of a better solution here. (ZivC)
if ( title_ != "" ) {
msg.from = "\"" + title_ + via_;
} else {
msg.from = from_;
}
// 'Subject' field
if ( itemTitle != null && itemTitle != "" ) {
msg.subject = itemTitle;
} else if ( itemLink != null && itemLink != "" ) {
msg.subject = "New post from " + title_ + " (" + itemLink + ")";
} else {
msg.subject = "New post from " + title_;
}
// 'To' field
msg.to = to_;
// We must make sure that the RFC 2822 headers do not have
// any HTML-encoded character.
// TODO: (ZivC) Do we really need this? Don't we do HtmlDecode before processing?
msg.from = System.Web.HttpUtility.HtmlDecode( msg.from );
msg.subject = System.Web.HttpUtility.HtmlDecode( msg.subject );
msg.to = System.Web.HttpUtility.HtmlDecode( msg.to );
// Body
// Massage the body to appear better
msg.body = new StringBuilder();
msg.body.Append( ""
+"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://w3.org/TR/xhtml11/DTD/xhtml1-transitional.dtd\">\r\n"
+"<html>\r\n"
+" <head>\r\n"
+" <style type='text/css'>\r\n"
+ GetStylesheet()
+" </style>\r\n"
+((baseUrl_ != "") ? " <base href='" + baseUrl_ + "'/>\r\n" : "")
+" </head>\r\n"
+" <body>\r\n" );
if ( parserComments_ != "" ) {
msg.body.AppendFormat(
"<p style='color: red'>The following information was salvaged from the feed.\r\n"
+ " Please forward the error description (found at the bottom of this page)"
+ " to the feed's provider.</p><hr />\r\n" );
}
} // ComposeMailHeader
private void ComposeMailFooter( ref MailHeaderAndBody msg ) {
msg.body.AppendFormat( "\r\n<p class='channeldata'>Channel: {0}\r\n", title_ );
if ( link_ != "" ) {
msg.body.AppendFormat( "<br />Link: <a href='{0}'>{0}</a>\r\n", link_ );
}
if ( description_ != "" ) {
msg.body.AppendFormat( "<br />Description: {0}\r\n", description_ );
}
msg.body.Append( "<br />Mail sent by <a href='http://bitworking.org/Aggie.html'>Aggie</a>.</p>\r\n" );
if ( parserComments_ != null && parserComments_ != "" ) {
msg.body.AppendFormat( "<hr /><p style='color: red'>Error description:</p>\r\n"
+ "<p style='color:red'>{0}</p>\r\n", parserComments_ );
}
msg.body.Append( " </body>\r\n</html>\r\n" );
} // ComposeMailFooter
private void ComposeMailItem( XmlNode node, ref MailHeaderAndBody msg ) {
string itemTitle = GetElementValue(node, "title");
string itemLink = GetElementValue(node, "link");
string itemDescription = GetElementValue(node, "description");
string itemDate = GetElementValue(node, "date");
// Note: We don't need to call StringUtils.StringOrFallback( *, "" ) here,
// as GetElementValue does this for us.
// Channel information is stored in instance data: title_, link_, description_.
// Item information is stored in formal arguments: itemTitle, itemLink, itemDescription.
//
// Mail appears as if sent from sender: "title_ (via Aggie)" <user@userhost>.
// Mail subject if item has a title: itemTitle,
// If has a link: itemLink,
// Otherwise: New post from title_.
ComposeMailItem( node, ref msg, itemTitle, itemLink, itemDescription, itemDate );
} // ComposeMailItem
private void ComposeMailItem( XmlNode node, ref MailHeaderAndBody msg, string itemTitle, string itemLink, string itemDescription, string itemDate ) {
// Body
msg.body.Append( "<div>\r\n" ); // Wrapper
// Massage the body to appear better
if ( itemTitle != "" ) {
if ( itemLink != "" ) {
msg.body.AppendFormat( "<h1><a href='{1}'>{0}</a></h1>\r\n", itemTitle, itemLink );
} else {
msg.body.AppendFormat( "<h1>{0}</h1>\r\n", itemTitle );
}
}
if ( itemLink != "" ) {
msg.body.AppendFormat( "<p class='rsslink'>Link: <a href='{0}'>{0}</a></p>\r\n", itemLink );
}
if ( itemDate != "" ) {
msg.body.AppendFormat( "<p class='rssdate'>Date: {0}</p>\r\n", itemDate );
}
msg.body.Append( itemDescription );
msg.body.Append( "\r\n</div>\r\n" ); // Wrapper
} // ComposeMailItem
private void ComposeMailFromItem( XmlNode node, out MailHeaderAndBody msg ) {
string itemTitle = GetElementValue(node, "title");
string itemLink = GetElementValue(node, "link");
string itemDescription = GetElementValue(node, "description");
string itemDate = GetElementValue(node, "date");
ComposeMailHeader( itemTitle, itemLink, out msg );
ComposeMailItem( node, ref msg, itemTitle, itemLink, itemDescription, itemDate );
ComposeMailFooter( ref msg );
} // ComposeMailFromItem
private void MailItem( SmtpConfigInfo config, SmtpBatchMailer batchMailer, MailHeaderAndBody msg ) {
if ( config.enableMailer ) {
bool enableMailer = config.enableMailer; // Anchor for debug breakpoints
SmtpMailItem mail = new SmtpMailItem();
mail.Body = msg.body.ToString();
mail.ContentTransferEncoding = ContentTransferEncoding.base64;
mail.From = msg.from;
mail.SendAsHtml = true;
mail.Subject = msg.subject;
mail.Recipients.Add( msg.to );
try {
//System.Diagnostics.Debug.WriteLine( "....Mail item to follow...." );
//System.Diagnostics.Debug.WriteLine( mail.Body );
if ( enableMailer ) {
batchMailer.SubmitItemForSending( mail );
}
}
catch ( System.Exception ) {
}
}
} // MailItem
// Embedded stylesheet support (by Tim Danner)
private const string STYLESHEET_FILENAME = "email.css";
private const string DEFAULT_STYLESHEET_FILENAME = "email.css.orig";
private string mailStylesheet_;
private string GetStylesheet() {
if (mailStylesheet_ == null) {
// Attempt to load email.css. Return its contents if successful,
// otherwise return a default stylesheet.
if (File.Exists(DEFAULT_STYLESHEET_FILENAME) && !File.Exists(STYLESHEET_FILENAME)) {
File.Copy(DEFAULT_STYLESHEET_FILENAME, STYLESHEET_FILENAME);
}
try {
using (StreamReader css_stream = new StreamReader(STYLESHEET_FILENAME)) {
mailStylesheet_ = css_stream.ReadToEnd();
}
}
catch (System.Exception) {
mailStylesheet_ = "";
}
}
return mailStylesheet_;
}
}
}
|