BradVin's .Net Blog

Code, snippets, controls, utils, etc. Basically all things .net

January 2008 - Posts

Creating vCalendars programmatically in C#

Have you ever wanted to send out meeting requests from your code? Well I wanted to do just that today. And I didnt want to reference the Outlook dll's. I simply wanted to send an email to an attendee's email address.

After much searching and alot of useless code I found a gem at http://chuckdotnet.blogspot.com. I took the code there and modified it slightly so that you can send the meeting request to multiple recipients (attendees) at once.

below is the method that creates the system.net.mailmessage object with all the necessary alternateviews, so that when opened in outlook it behaves exactly like a meeting request that was sent from within outlook. remember to add using System.Net.Mail;using System.Net.Mime; to the top of your code.


public static MailMessage CreateMeetingRequest(DateTime start, DateTime end, string subject, string summary,
string location, string organizerName, string organizerEmail, string attendeeName, string attendeeEmail)
{
MailAddressCollection col = new MailAddressCollection();
col.Add(new MailAddress(attendeeEmail, attendeeName));
return CreateMeetingRequest(start, end, subject, summary, location, organizerName, organizerEmail, col);
}

public static MailMessage CreateMeetingRequest(DateTime start, DateTime end, string subject, string summary,
string location, string organizerName, string organizerEmail, MailAddressCollection attendeeList)
{
MailMessage msg = new MailMessage();

// Set up the different mime types contained in the message
System.Net.Mime.ContentType textType = new System.Net.Mime.ContentType("text/plain");
System.Net.Mime.ContentType HTMLType = new System.Net.Mime.ContentType("text/html");
System.Net.Mime.ContentType calendarType = new System.Net.Mime.ContentType("text/calendar");

// Add parameters to the calendar header
calendarType.Parameters.Add("method", "REQUEST");
calendarType.Parameters.Add("name", "meeting.ics");

// Create message body parts
// create the Body in text format
string bodyText = "Type:Single Meeting\r\nOrganizer: {0}\r\nStart Time:{1}\r\nEnd Time:{2}\r\nTime Zone:{3}\r\nLocation: {4}\r\n\r\n*~*~*~*~*~*~*~*~*~*\r\n\r\n{5}";
bodyText = string.Format(bodyText,
organizerName,
start.ToLongDateString() + " " + start.ToLongTimeString(),
end.ToLongDateString() + " " + end.ToLongTimeString(),
System.TimeZone.CurrentTimeZone.StandardName,
location,
summary);

AlternateView textView = AlternateView.CreateAlternateViewFromString(bodyText, textType);
msg.AlternateViews.Add(textView);

//create the Body in HTML format
string bodyHTML = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2//EN\">\r\n<HTML>\r\n<HEAD>\r\n<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=utf-8\">\r\n<META NAME=\"Generator\" CONTENT=\"MS Exchange Server version 6.5.7652.24\">\r\n<TITLE>{0}</TITLE>\r\n</HEAD>\r\n<BODY>\r\n<!-- Converted from text/plain format -->\r\n<P><FONT SIZE=2>Type:Single Meeting<BR>\r\nOrganizer:{1}<BR>\r\nStart Time:{2}<BR>\r\nEnd Time:{3}<BR>\r\nTime Zone:{4}<BR>\r\nLocation:{5}<BR>\r\n<BR>\r\n*~*~*~*~*~*~*~*~*~*<BR>\r\n<BR>\r\n{6}<BR>\r\n</FONT>\r\n</P>\r\n\r\n</BODY>\r\n</HTML>";
bodyHTML = string.Format(bodyHTML,
summary,
organizerName,
start.ToLongDateString() + " " + start.ToLongTimeString(),
end.ToLongDateString() + " " + end.ToLongTimeString(),
System.TimeZone.CurrentTimeZone.StandardName,
location,
summary);

AlternateView HTMLView = AlternateView.CreateAlternateViewFromString(bodyHTML, HTMLType);
msg.AlternateViews.Add(HTMLView);

//create the Body in VCALENDAR format
string calDateFormat = "yyyyMMddTHHmmssZ";
string bodyCalendar = "BEGIN:VCALENDAR\r\nMETHOD:REQUEST\r\nPRODID:Microsoft CDO for Microsoft Exchange\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:(GMT-06.00) Central Time (US & Canada)\r\nX-MICROSOFT-CDO-TZID:11\r\nBEGIN:STANDARD\r\nDTSTART:16010101T020000\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nRRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=11;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nDTSTART:16010101T020000\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nRRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=3;BYDAY=2SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTAMP:{8}\r\nDTSTART:{0}\r\nSUMMARY:{7}\r\nUID:{5}\r\nATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=\"{9}\":MAILTO:{9}\r\nACTION;RSVP=TRUE;CN=\"{4}\":MAILTO:{4}\r\nORGANIZER;CN=\"{3}\":mailto:{4}\r\nLOCATION:{2}\r\nDTEND:{1}\r\nDESCRIPTION:{7}\\N\r\nSEQUENCE:1\r\nPRIORITY:5\r\nCLASS:\r\nCREATED:{8}\r\nLAST-MODIFIED:{8}\r\nSTATUS:CONFIRMED\r\nTRANSP:OPAQUE\r\nX-MICROSOFT-CDO-BUSYSTATUS:BUSY\r\nX-MICROSOFT-CDO-INSTTYPE:0\r\nX-MICROSOFT-CDO-INTENDEDSTATUS:BUSY\r\nX-MICROSOFT-CDO-ALLDAYEVENT:FALSE\r\nX-MICROSOFT-CDO-IMPORTANCE:1\r\nX-MICROSOFT-CDO-OWNERAPPTID:-1\r\nX-MICROSOFT-CDO-ATTENDEE-CRITICAL-CHANGE:{8}\r\nX-MICROSOFT-CDO-OWNER-CRITICAL-CHANGE:{8}\r\nBEGIN:VALARM\r\nACTION:DISPLAY\r\nDESCRIPTION:REMINDER\r\nTRIGGER;RELATED=START:-PT00H15M00S\r\nEND:VALARM\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n";
bodyCalendar = string.Format(bodyCalendar,
start.ToUniversalTime().ToString(calDateFormat),
end.ToUniversalTime().ToString(calDateFormat),
location,
organizerName,
organizerEmail,
Guid.NewGuid().ToString("B"),
summary,
subject,
DateTime.Now.ToUniversalTime().ToString(calDateFormat),
attendeeList.ToString());

AlternateView calendarView = AlternateView.CreateAlternateViewFromString(bodyCalendar, calendarType);
calendarView.TransferEncoding = TransferEncoding.SevenBit;
msg.AlternateViews.Add(calendarView);

// Adress the message
msg.From = new MailAddress(organizerEmail);
foreach (MailAddress attendee in attendeeList)
{
msg.To.Add(attendee);
}
msg.Subject = subject;

return msg;
}
Posted: Jan 16 2008, 08:29 PM by bradvin | with 11 comment(s)
Filed under: ,
UberUtils - Part 5 : Configuration

ÜberUtils Series posts so far :

**** Please note that this code is using .NET 3.5 ****

Now onto the post - everyone I'm sure has used the system.configuration namespace before in a project. If you haven't then shame on you. If you have, then you know it should be used for storing things like :

  • admin email address
  • connection strings to a database
  • log file path
  • Tax amounts

and so on. I am a strong believer in NEVER hard code a setting. There is just no reason to do so. What happens if a setting changes down the line? Lets use a webmaster email as an example. You would have to locate wherever you use the email address (probably more than one place) and edit it. Then recompile and redeploy your changes. What a mission! Instead, just store it in the appsettings section of the .config file and edit the setting there whenever you need to.

I really do like using the appsettings and connectionstring sections in the .config files, but I think there is room for improvement. Wouldn't it be nice if appsettings was strongly typed for types other than string? Indeed it would. Here's an example: you store a "Testing" appsetting that determines if your website is in testing mode. If it is in testing mode, then you send all emails to Admin instead of sending emails to the actual users. Now every time you want to check if the site is in testing mode you have to convert the appsetting to a boolean first. Similarly with a Tax appsetting. You have to convert it to decimal every time before you can use it.

This then follows directly onto my next question : Wouldn't it be nice if appsettings and connectionstrings had default values? Yes again. Let's say the "testing" appsetting we spoke about earlier wasn't in the .config file. We would then want it to default to true. Maybe our tax appsetting should default to 0.14 and maybe we always use the same default connectionstring in our data access layer. Defaults would be great.

I address these two shortfalls with Utils.Configuration. Lets look at some examples:

bool bTesting = Utils.Configuration.ConfigurationManager.AppSettings["Testing", true];
this gets the testing appsetting with a deafult of true (if we forgot to add the appsetting into the .config file). Notice too how it's strongly typed to be boolean. You could've also done it this way :
bool bTesting = Utils.Configuration.ConfigurationManager.AppSettings.GetValue("Testing", true);

This goes the same for the other types : Decimals, Ints, Doubles, Datetimes and obviously strings.

decimal nTax = Utils.Configuration.ConfigurationManager.AppSettings["Tax", 0.14M];
The same applies for connectionstrings, except the output is obviously always a string. You can also check if a connection string exists so you can provide better error handling :
if (ConfigurationManager.ConnectionStrings.Exists("TestConn2"))
{
return ConfigurationManager.ConnectionStrings["TestConn2", "Driver={SQL Server};Server=myServerAddress;Database=myDataBase;Uid=myUsername;Pwd=myPassword;"];
}
download the assembly with unit tests here
Posted: Jan 16 2008, 06:07 PM by bradvin | with 3 comment(s)
Filed under: , ,
More Posts