How to generate an iCalendar entry that works on Outlook, Android and iOS

做~自己de王妃 提交于 2019-12-11 16:11:17

问题


The task was to send a bunch of emails with appointments from an Excel on a regular bases. For that purpose, I included macros that generated iCalendar files, attached them to an email and send them automatically.

Problem:

The email part was the easiest one. But not the iCalendar entry! If you simply generate an entry with start and end time, it is importable by Outlook and shows the correct times in your calendar. But if you try to import this entry in your Android phone or iPhone, you get wrong times or even error messages.

The biggest problem is the fact, that Android treated everything as UTC coded. Ok, then create the iCalendar entry in UTC and the problem is solved. But that sounds easier than done. Especially when you live in Europe and have to deal with daylight saving and sending of emails with iCalendar entries that are stepping over the saving start or end date.

Solution:

To calculate correct conversion from the local time to UTC, one can use two function calls into the Kernel32 module of Windows to do all the math:

GetTimeZoneInformation and TzSpecificLocalTimeToSystemTime

To check whether your iCalendar entry is correct, there are website that offer online validation. I used this one: https://icalendar.org/validator.html. If this validator doesn't complain anymore, you're good. The iCalendar entry generated from the code below is now accepted from all applications and produces the correct times everywhere.

Start the routine TestIt to see how it works.

The current code only generates an email, the user has to press the send button manually. But if you move the comment tic from '.Send to .Display, the email will be send without further notice. You will find it in your send folder.

Screenshot from the composed email

Here is the code:

Option Explicit


' For time & date conversion from CET/CEST to UTC see
' https://docs.microsoft.com/en-us/windows/desktop/api/timezoneapi/nf-timezoneapi-gettimezoneinformation
' and
' https://docs.microsoft.com/en-us/windows/desktop/api/timezoneapi/nf-timezoneapi-tzspecificlocaltimetosystemtime

Private Type SYSTEMTIME
  wYear As Integer
  wMonth As Integer
  wDayOfWeek As Integer
  wDay As Integer
  wHour As Integer
  wMinute As Integer
  wSecond As Integer
  wMilliseconds As Integer
End Type

Private Type TIME_ZONE_INFORMATION
  Bias As Long
  StandardName(0 To 31) As Integer
  StandardDate As SYSTEMTIME
  StandardBias As Long
  DaylightName(0 To 31) As Integer
  DaylightDate As SYSTEMTIME
  DaylightBias As Long
End Type

Private Declare Function GetTimeZoneInformation Lib "Kernel32" _
  (lpTimeZoneInformation As TIME_ZONE_INFORMATION) As Long

Private Declare Function TzSpecificLocalTimeToSystemTime Lib "Kernel32" _
  (lpTimeZoneInformation As TIME_ZONE_INFORMATION, _
   lpLocalTime As SYSTEMTIME, lpUniversalTime As SYSTEMTIME) As Integer

Public objFSO As New FileSystemObject


Private Sub TestIt()
  Call SendTextEmailWith_iCalendar("joe.sixpack@example.com", "2019-02-28 14:00", "2019-02-28 14:30")
End Sub


' This subroutine creates / sends a plain text formatted email with an iCalendar entry as attachment.
Private Sub SendTextEmailWith_iCalendar( _
  EmailAddress As String, _
  EventStart As Date, _
  EventEnd As Date _
)
  Dim objOutlook As Object
  Dim objMail As Object
  Dim TempIcsFilename As String

  ' Put date, start and end time into filename.
  TempIcsFilename = Environ$("temp") & "\iCalendar Entry " & Format(EventStart, "YYYYMMDD") & " " & _
                    Format(EventStart, "hhmm") & "-" & Format(EventEnd, "hhmm") & ".ics"
  Call Create_iCalendar_File(TempIcsFilename, EventStart, EventEnd, "This is the summary", _
                              "This is the location", 5)

  Set objOutlook = CreateObject("Outlook.Application")
  Set objMail = objOutlook.CreateItem(0)

  With objMail
   .To = EmailAddress
   .Subject = "Your appointment at " & Format(EventStart, "YYYY-MM-DD"" at ""hh:mm"" hours""")
   .Body = "This is an automated email with an iCalendar entry for your next appointment."
   .Attachments.Add TempIcsFilename
   .Display ' Displays only the composed email. The user has to send it.
   '.Send   ' Sends the composed email without further query.
  End With

  Kill TempIcsFilename
  Set objMail = Nothing
  Set objOutlook = Nothing
End Sub


' Creates an iCalendar file of the given arguments with UTC coded start-date and end-date.
' This will then be accepted by Android and iOS calendar apps as well as MS Outlook with the correct
' time local time.
Private Sub Create_iCalendar_File( _
  Filename As String, _
  EventStart As Date, _
  EventEnd As Date, _
  Summary As String, _
  Optional Location As String = "", _
  Optional Reminder As Integer = 15 _
)
  Dim fso As Object
  Dim TimeZoneInfo As TIME_ZONE_INFORMATION
  Dim UTC As SYSTEMTIME
  Dim LocalTime As SYSTEMTIME

  ' Get the time zone info of the system settings.
  Call GetTimeZoneInformation(TimeZoneInfo)

  ' Create an iCalendar file for the specified time window. Some of the properties seem superfluous
  ' or redundant at first glance. Ommitting them leads to warnings / errors in some calendar tools.
  ' Use iCalendar Validator from https://icalendar.org/validator.html to check your entry.
  Set fso = objFSO.CreateTextFile(Filename, Overwrite:=True)
  With fso
    .WriteLine ("BEGIN:VCALENDAR")
    .WriteLine ("PRODID:-//<Replace this with your program/company info>//EN")
    .WriteLine ("VERSION:2.0")
    .WriteLine ("BEGIN:VEVENT")
    .WriteLine ("UID:<Make this unique for your program>-" & Format(Now(), "YYYYMMDD""-""hhmmss"))
    ' Timezone doesn't matter for the time stamp.
    .WriteLine ("DTSTAMP:" & Format(Now(), "YYYYMMDD""T""hhmmss"))

    LocalTime = DateToSystemTime(EventStart)
    Call TzSpecificLocalTimeToSystemTime(TimeZoneInfo, LocalTime, UTC)
    .WriteLine ("DTSTART:" & SystemTimeToDTString(UTC))

    LocalTime = DateToSystemTime(EventEnd)
    Call TzSpecificLocalTimeToSystemTime(TimeZoneInfo, LocalTime, UTC)
    .WriteLine ("DTEND:" & SystemTimeToDTString(UTC))

    .WriteLine ("LOCATION:" & Location)
    .WriteLine ("PRIORITY:5") ' Normal priority
    .WriteLine ("SEQUENCE:0")
    .WriteLine ("SUMMARY:" & Summary)
    .WriteLine ("BEGIN:VALARM")
    .WriteLine ("TRIGGER:-PT" & Reminder & "M")
    .WriteLine ("ACTION:DISPLAY")
    .WriteLine ("DESCRIPTION:Reminder")
    .WriteLine ("END:VALARM")
    .WriteLine ("END:VEVENT")
    .WriteLine ("END:VCALENDAR")
    .Close
  End With

  Set fso = Nothing
End Sub


' Convert Date into SYSTEMTIME
Private Function DateToSystemTime(TimeStamp As Date) As SYSTEMTIME
  With DateToSystemTime
    .wYear = Year(TimeStamp)
    .wMonth = Month(TimeStamp)
    .wDay = Day(TimeStamp)
    .wHour = Hour(TimeStamp)
    .wMinute = Minute(TimeStamp)
    .wSecond = Second(TimeStamp)
    .wMilliseconds = 0
  End With
End Function


' Convert SYSTEMTIME into a DTSTART/DTEND string
Private Function SystemTimeToDTString(TimeStamp As SYSTEMTIME) As String
  With TimeStamp
    SystemTimeToDTString = Format(DateSerial(.wYear, .wMonth, .wDay) + _
                                  TimeSerial(.wHour, .wMinute, .wSecond), _
                                  "YYYYMMDD""T""hhmmss""Z""")
  End With
End Function

来源:https://stackoverflow.com/questions/54876217/how-to-generate-an-icalendar-entry-that-works-on-outlook-android-and-ios

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!