Creating Appointments (vCalendar) in C#

3/13/2008

To be honest I used the code here and here to get started. And finding even those two items was rather painful. However manipulating an appointment on an exchange server is actually pretty easy once you get used to WebDAV. However finding the info to help you do just that is a little daunting. That being said I've been able to cobble together a class that can create an appointment/meeting and save it to a person's calendar, email that appointment to the various invitees, send out cancellations of that meeting, and also delete an appointment from someone's calendar.

   1: /*
   2: Copyright (c) 2010 <a href="http://www.gutgames.com">James Craig</a>
   3: 
   4: Permission is hereby granted, free of charge, to any person obtaining a copy
   5: of this software and associated documentation files (the "Software"), to deal
   6: in the Software without restriction, including without limitation the rights
   7: to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   8: copies of the Software, and to permit persons to whom the Software is
   9: furnished to do so, subject to the following conditions:
  10: 
  11: The above copyright notice and this permission notice shall be included in
  12: all copies or substantial portions of the Software.
  13: 
  14: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15: IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16: FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17: AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18: LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19: OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  20: THE SOFTWARE.*/
  21:  
  22: #region Usings
  23: using System;
  24: using System.Collections.Generic;
  25: using System.Net;
  26: using System.Net.Mail;
  27: using System.Text;
  28: #endregion
  29:  
  30:  
  31: namespace Utilities.Exchange
  32: {
  33:     /// <summary>
  34:     /// Utility class for creating and sending an appointment
  35:     /// </summary>
  36:     public class Appointment
  37:     {
  38:         #region Constructor
  39:         /// <summary>
  40:         /// Constructor
  41:         /// </summary>
  42:         public Appointment()
  43:         {
  44:             AttendeeList = new MailAddressCollection();
  45:             Attachments = new List<Attachment>();
  46:         }
  47:         #endregion
  48:  
  49:         #region Public Functions
  50:  
  51:         /// <summary>
  52:         /// Adds an appointment to a user's calendar
  53:         /// </summary>
  54:         public virtual void AddAppointment()
  55:         {
  56:             string XMLNSInfo = "xmlns:g=\"DAV:\" "
  57:                 + "xmlns:e=\"http://schemas.microsoft.com/exchange/\" "
  58:                 + "xmlns:mapi=\"http://schemas.microsoft.com/mapi/\" "
  59:                 + "xmlns:mapit=\"http://schemas.microsoft.com/mapi/proptag/\" "
  60:                 + "xmlns:x=\"xml:\" xmlns:cal=\"urn:schemas:calendar:\" "
  61:                 + "xmlns:dt=\"urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/\" "
  62:                 + "xmlns:header=\"urn:schemas:mailheader:\" "
  63:                 + "xmlns:mail=\"urn:schemas:httpmail:\"";
  64:  
  65:             string CalendarInfo = "<cal:location>" + Location + "</cal:location>"// + Location + "</cal:location>"
  66:                 + "<cal:dtstart dt:dt=\"dateTime.tz\">" + StartDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.000Z") + "</cal:dtstart>"// + StartDate.ToUniversalTime().ToString("yyyyMMddTHHmmssZ") + "</cal:dtstart>"
  67:                 + "<cal:dtend dt:dt=\"dateTime.tz\">" + EndDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.000Z") + "</cal:dtend>"// + EndDate.ToUniversalTime().ToString("yyyyMMddTHHmmssZ") + "</cal:dtend>"
  68:                 + "<cal:instancetype dt:dt=\"int\">0</cal:instancetype>"
  69:                 + "<cal:busystatus>BUSY</cal:busystatus>"
  70:                 + "<cal:meetingstatus>CONFIRMED</cal:meetingstatus>"
  71:                 + "<cal:alldayevent dt:dt=\"boolean\">0</cal:alldayevent>"
  72:                 + "<cal:responserequested dt:dt=\"boolean\">1</cal:responserequested>"
  73:                 + "<cal:reminderoffset dt:dt=\"int\">900</cal:reminderoffset>"
  74:                 + "<cal:uid>" + MeetingGUID.ToString("B") + "</cal:uid>";
  75:  
  76:             string HeaderInfo = "<header:to>" + AttendeeList.ToString() + "</header:to>";
  77:  
  78:             string MailInfo = "<mail:subject>" + Subject + "</mail:subject>"
  79:                 + "<mail:htmldescription>" + Summary + "</mail:htmldescription>";
  80:  
  81:             string AppointmentRequest = "<?xml version=\"1.0\"?>"
  82:                 + "<g:propertyupdate " + XMLNSInfo + ">"
  83:                 + "<g:set><g:prop>"
  84:                 + "<g:contentclass>urn:content-classes:appointment</g:contentclass>"
  85:                 + "<e:outlookmessageclass>IPM.Appointment</e:outlookmessageclass>"
  86:                 + MailInfo
  87:                 + CalendarInfo
  88:                 + HeaderInfo
  89:                 + "<mapi:finvited dt:dt=\"boolean\">1</mapi:finvited>"
  90:                 + "</g:prop></g:set>"
  91:                 + "</g:propertyupdate>";
  92:  
  93:             System.Net.HttpWebRequest PROPPATCHRequest = (System.Net.HttpWebRequest)HttpWebRequest.Create(ServerName + "/exchange/" + Directory + "/Calendar/" + MeetingGUID.ToString() + ".eml");
  94:  
  95:             System.Net.CredentialCache MyCredentialCache = new System.Net.CredentialCache();
  96:             if (!string.IsNullOrEmpty(UserName) && !string.IsNullOrEmpty(Password))
  97:             {
  98:                 MyCredentialCache.Add(new System.Uri(ServerName + "/exchange/" + Directory + "/Calendar/" + MeetingGUID.ToString() + ".eml"),
  99:                                        "NTLM",
 100:                                        new System.Net.NetworkCredential(UserName, Password));
 101:             }
 102:             else
 103:             {
 104:                 MyCredentialCache.Add(new System.Uri(ServerName + "/exchange/" + Directory + "/Calendar/" + MeetingGUID.ToString() + ".eml"),
 105:                                        "Negotiate",
 106:                                        (System.Net.NetworkCredential)CredentialCache.DefaultCredentials);
 107:             }
 108:  
 109:             PROPPATCHRequest.Credentials = MyCredentialCache;
 110:             PROPPATCHRequest.Method = "PROPPATCH";
 111:             byte[] bytes = Encoding.UTF8.GetBytes((string)AppointmentRequest);
 112:             PROPPATCHRequest.ContentLength = bytes.Length;
 113:             using (System.IO.Stream PROPPATCHRequestStream = PROPPATCHRequest.GetRequestStream())
 114:             {
 115:                 PROPPATCHRequestStream.Write(bytes, 0, bytes.Length);
 116:                 PROPPATCHRequestStream.Close();
 117:                 PROPPATCHRequest.ContentType = "text/xml";
 118:                 System.Net.WebResponse PROPPATCHResponse = (System.Net.HttpWebResponse)PROPPATCHRequest.GetResponse();
 119:                 PROPPATCHResponse.Close();
 120:             }
 121:         }
 122:  
 123:         /// <summary>
 124:         /// Emails an appointment to the specified users
 125:         /// </summary>
 126:         public virtual void EmailAppointment()
 127:         {
 128:             using (MailMessage Mail = new MailMessage())
 129:             {
 130:                 System.Net.Mime.ContentType TextType = new System.Net.Mime.ContentType("text/plain");
 131:                 using (AlternateView TextView = AlternateView.CreateAlternateViewFromString(GetText(), TextType))
 132:                 {
 133:                     System.Net.Mime.ContentType HTMLType = new System.Net.Mime.ContentType("text/html");
 134:                     using (AlternateView HTMLView = AlternateView.CreateAlternateViewFromString(GetHTML(false), HTMLType))
 135:                     {
 136:                         System.Net.Mime.ContentType CalendarType = new System.Net.Mime.ContentType("text/calendar");
 137:                         CalendarType.Parameters.Add("method", "REQUEST");
 138:                         CalendarType.Parameters.Add("name", "meeting.ics");
 139:                         using (AlternateView CalendarView = AlternateView.CreateAlternateViewFromString(GetCalendar(false), CalendarType))
 140:                         {
 141:                             CalendarView.TransferEncoding = System.Net.Mime.TransferEncoding.SevenBit;
 142:  
 143:                             Mail.AlternateViews.Add(TextView);
 144:                             Mail.AlternateViews.Add(HTMLView);
 145:                             Mail.AlternateViews.Add(CalendarView);
 146:  
 147:                             Mail.From = new MailAddress(OrganizerEmail);
 148:  
 149:                             foreach (MailAddress attendee in AttendeeList)
 150:                             {
 151:                                 Mail.To.Add(attendee);
 152:                             }
 153:  
 154:  
 155:                             Mail.Subject = Subject;
 156:  
 157:                             foreach (Attachment Attachment in Attachments)
 158:                             {
 159:                                 Mail.Attachments.Add(Attachment);
 160:                             }
 161:  
 162:  
 163:                             SmtpClient Server = new SmtpClient(ServerName, Port);
 164:                             if (!string.IsNullOrEmpty(UserName) && !string.IsNullOrEmpty(Password))
 165:                             {
 166:                                 Server.Credentials = new System.Net.NetworkCredential(UserName, Password);
 167:                             }
 168:                             if (AttendeeList.Count > 0)
 169:                             {
 170:                                 Server.Send(Mail);
 171:                             }
 172:                         }
 173:                     }
 174:                 }
 175:             }
 176:         }
 177:  
 178:         /// <summary>
 179:         /// Sends a cancellation to the people specified
 180:         /// </summary>
 181:         public virtual void SendCancelEmails()
 182:         {
 183:             using (MailMessage Mail = new MailMessage())
 184:             {
 185:                 System.Net.Mime.ContentType TextType = new System.Net.Mime.ContentType("text/plain");
 186:                 using (AlternateView TextView = AlternateView.CreateAlternateViewFromString(GetText(), TextType))
 187:                 {
 188:                     System.Net.Mime.ContentType HTMLType = new System.Net.Mime.ContentType("text/html");
 189:                     using (AlternateView HTMLView = AlternateView.CreateAlternateViewFromString(GetHTML(true), HTMLType))
 190:                     {
 191:                         System.Net.Mime.ContentType CalendarType = new System.Net.Mime.ContentType("text/calendar");
 192:                         CalendarType.Parameters.Add("method", "CANCEL");
 193:                         CalendarType.Parameters.Add("name", "meeting.ics");
 194:                         using (AlternateView CalendarView = AlternateView.CreateAlternateViewFromString(GetCalendar(true), CalendarType))
 195:                         {
 196:                             CalendarView.TransferEncoding = System.Net.Mime.TransferEncoding.SevenBit;
 197:  
 198:                             Mail.AlternateViews.Add(TextView);
 199:                             Mail.AlternateViews.Add(HTMLView);
 200:                             Mail.AlternateViews.Add(CalendarView);
 201:  
 202:                             Mail.From = new MailAddress(OrganizerEmail);
 203:                             foreach (MailAddress attendee in AttendeeList)
 204:                             {
 205:                                 Mail.To.Add(attendee);
 206:                             }
 207:                             Mail.Subject = Subject + " - Cancelled";
 208:                             foreach (Attachment Attachment in Attachments)
 209:                             {
 210:                                 Mail.Attachments.Add(Attachment);
 211:                             }
 212:                             SmtpClient Server = new SmtpClient(ServerName, Port);
 213:                             if (!string.IsNullOrEmpty(UserName) && !string.IsNullOrEmpty(Password))
 214:                             {
 215:                                 Server.Credentials = new System.Net.NetworkCredential(UserName, Password);
 216:                             }
 217:                             if (AttendeeList.Count > 0)
 218:                             {
 219:                                 Server.Send(Mail);
 220:                             }
 221:                         }
 222:                     }
 223:                 }
 224:             }
 225:         }
 226:  
 227:         /// <summary>
 228:         /// Cancels an appointment on someone's calendar
 229:         /// </summary>
 230:         public virtual void CancelAppointment()
 231:         {
 232:             System.Net.HttpWebRequest PROPPATCHRequest = (System.Net.HttpWebRequest)HttpWebRequest.Create(ServerName + "/exchange/" + Directory + "/Calendar/" + MeetingGUID.ToString() + ".eml");
 233:  
 234:             System.Net.CredentialCache MyCredentialCache = new System.Net.CredentialCache();
 235:             if (!string.IsNullOrEmpty(UserName) && !string.IsNullOrEmpty(Password))
 236:             {
 237:                 MyCredentialCache.Add(new System.Uri(ServerName + "/exchange/" + Directory + "/Calendar/" + MeetingGUID.ToString() + ".eml"),
 238:                                    "NTLM",
 239:                                    new System.Net.NetworkCredential(UserName, Password));
 240:             }
 241:             else
 242:             {
 243:                 MyCredentialCache.Add(new System.Uri(ServerName + "/exchange/" + Directory + "/Calendar/" + MeetingGUID.ToString() + ".eml"),
 244:                                        "Negotiate",
 245:                                        (System.Net.NetworkCredential)CredentialCache.DefaultCredentials);
 246:             }
 247:  
 248:             PROPPATCHRequest.Credentials = MyCredentialCache;
 249:             PROPPATCHRequest.Method = "DELETE";
 250:             System.Net.WebResponse PROPPATCHResponse = (System.Net.HttpWebResponse)PROPPATCHRequest.GetResponse();
 251:             PROPPATCHResponse.Close();
 252:         }
 253:  
 254:         #endregion
 255:  
 256:         #region Private Functions
 257:  
 258:         /// <summary>
 259:         /// Returns the text version of the appointment
 260:         /// </summary>
 261:         /// <returns>A text version of the appointment</returns>
 262:         private string GetText()
 263:         {
 264:             string Body = "Type:Single Meeting\n" +
 265:                 "Organizer:" + OrganizerName + "\n" +
 266:                 "Start Time:" + StartDate.ToLongDateString() + " " + StartDate.ToLongTimeString() + "\n" +
 267:                 "End Time:" + EndDate.ToLongDateString() + " " + EndDate.ToLongTimeString() + "\n" +
 268:                 "Time Zone:" + System.TimeZone.CurrentTimeZone.StandardName + "\n" +
 269:                 "Location: " + Location + "\n\n" +
 270:                 "*~*~*~*~*~*~*~*~*~*\n\n" +
 271:                 Summary;
 272:             return Body;
 273:         }
 274:  
 275:         /// <summary>
 276:         /// Gets an HTML version of the appointment
 277:         /// </summary>
 278:         /// <param name="Canceled">If true it returns a cancellation, false it returns a request</param>
 279:         /// <returns>An HTML version of the appointment</returns>
 280:         private string GetHTML(bool Canceled)
 281:         {
 282:             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>";
 283:             string TempSummary = Summary;
 284:             if (Canceled)
 285:             {
 286:                 TempSummary += " - Canceled";
 287:             }
 288:             return string.Format(bodyHTML,
 289:                 TempSummary,
 290:                 OrganizerName,
 291:                 StartDate.ToLongDateString() + " " + StartDate.ToLongTimeString(),
 292:                 EndDate.ToLongDateString() + " " + EndDate.ToLongTimeString(),
 293:                 System.TimeZone.CurrentTimeZone.StandardName,
 294:                 Location,
 295:                 TempSummary);
 296:         }
 297:  
 298:         /// <summary>
 299:         /// Gets an iCalendar version of the appointment
 300:         /// </summary>
 301:         /// <param name="Canceled">If true, it returns a cancellation version.
 302:         /// If false, it returns a request version.</param>
 303:         /// <returns>An iCalendar version of the appointment</returns>
 304:         private string GetCalendar(bool Canceled)
 305:         {
 306:             string DateFormatUsing = "yyyyMMddTHHmmssZ";
 307:             string Method;
 308:             if (!Canceled)
 309:                 Method = "REQUEST";
 310:             else
 311:                 Method = "CANCEL";
 312:             string bodyCalendar = "BEGIN:VCALENDAR\r\nMETHOD:{10}\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";
 313:             bodyCalendar = string.Format(bodyCalendar,
 314:                 StartDate.ToUniversalTime().ToString(DateFormatUsing),
 315:                 EndDate.ToUniversalTime().ToString(DateFormatUsing),
 316:                 Location,
 317:                 OrganizerName,
 318:                 OrganizerEmail,
 319:                 MeetingGUID.ToString("B"),
 320:                 Summary,
 321:                 Subject,
 322:                 DateTime.Now.ToUniversalTime().ToString(DateFormatUsing),
 323:                 AttendeeList.ToString(),
 324:                 Method);
 325:             return bodyCalendar;
 326:         }
 327:  
 328:         #endregion
 329:  
 330:         #region Variables
 331:         public DateTime StartDate;
 332:         public DateTime EndDate;
 333:         public string Subject;
 334:         public string Summary;
 335:         public string Location;
 336:         public MailAddressCollection AttendeeList;
 337:         public string OrganizerName;
 338:         public string OrganizerEmail;
 339:  
 340:         public List<Attachment> Attachments;
 341:  
 342:         public Guid MeetingGUID;
 343:  
 344:         public string Directory;
 345:         public string ServerName;
 346:         public string UserName;
 347:         public string Password;
 348:         public int Port;
 349:  
 350:         #endregion
 351:     }
 352: }
The code is apart of my utilities library and as such might have been updated since this posting. Anyway, you may run into an issue with the code if you are using Exchange 2010. In 2010, WebDAV was deprecated and the code above uses WebDAV to communicate with the server. Unfortunately I do not have a copy of Exchange 2010 to play around with so I haven't added the code for that situation. But if you're running an earlier version, it should work.


Comments

James Craig
June 17, 2010 10:54 AM

I'm assuming it wouldn't work. I mean the code would have to be modified. That of course assumes that WebDAV is supported by Lotus (I don't know as my only experience with it has been migrating off of it).

kamal
June 17, 2010 2:32 AM

Halo, is there different, if i use lotus domino as email server ?

James Craig
November 12, 2009 9:31 AM

StringBuilder is generally faster than concatenating strings. There is of course the trade off of creating the StringBuilder object but since it acts like a vector when it comes to allocation for the strings themselves, it runs a bit faster.

Spain Jobs
November 12, 2009 2:23 AM

What???s the advantage of using System.Text.StringBuilder over System.String?