Search code examples
outlookgmailsendgridicalendarsendgrid-api-v3

How to send .ics calendar invite through SendGrid so that it renders in email clients?


I'm trying to send .ics calendar invites through SendGrid (from Node server) so that it renders in clients like Outlook or Gmail as an actual invitation (with accept/decline buttons) and not just as an attachment file.

I've spent days researching this (dozens of Stackoverflow questions, RFC-5545, RFC-2446, iCalendar Specification Excerpts, Sendgrid's GitHub issues threads: 1, 2, 3, SendGrid docs, sources etc).

However, there just doesn't seem to be an answer for this (or am I missing something out?).


What I've found so far is that Content-Type for the attachment is very important here, especially, method=REQUEST part. And that even the order of properties in the file makes difference.

Despite a lot of questions here on SO, most of them remain unanswered for some reason.


Here's how I set up my attachment object:

const SendGrid = require("@sendgrid/mail");

  const attachment = {
    filename: 'invite.ics',
    name: 'invite.ics',
    content: Buffer.from(data).toString('base64'),
    disposition: 'attachment',
    contentId: uuid(),
    type: 'application/ics'
  };

SendGrid.send({
      attachments: [attachment],
      templateId,
      from: {
        email: config.emailSender,
        name: config.emailName,
      },
      to: user.email,
      dynamicTemplateData: {
        ...rest,
        user,
      },
      headers: {
        'List-Unsubscribe': `<mailto:unsubscribe.link`,
      },
    });

As for type property, I've tried the following variants:

1. type: 'text/calendar; method=REQUEST'
2. type: 'application/ics'
3. type: 'text/calendar;method=REQUEST;name=\"invite.ics\"'
4. type: 'text/calendar; method=REQUEST; charset=UTF-8; component=vevent'
5. type: 'text/calendar'

However, nothing works except 'text/calendar' and 'application/ics' (and there doesn't seem to be any difference between them).

Content-Type is a reserved header according to the SendGrid docs, so it's not possible to set it somehow through headers property or smth.

The disposition: 'inline' option also doesn't work at all (only disposition: 'attachment').


Here's how the .ics file I generate looks like:

BEGIN:VCALENDAR
PRODID:-//Organization//Organization App//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:REQUEST
BEGIN:VEVENT
DTSTART:20210426T160000Z
DTEND:20210426T170000Z
DTSTAMP:20210418T134622Z
ORGANIZER;CN=John Smith:MAILTO:[email protected]
UID:dcfd5905-be85-4c8f-8a27-475b0ec67d8b
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=John Smith;X-NUM-GUESTS=0:MAILTO:[email protected]
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=John Test;X-NUM-GUESTS=0:MAILTO:[email protected]
CREATED:20210418T134622Z
DESCRIPTION:my description
LAST-MODIFIED:20210418T134622Z
LOCATION:https://location.url
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:my summary
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR

The file is perfectly valid and opens seamlessly in iCalendar.

But why doesn't this get rendered in Outlook or Gmail?

Currently, the only way to add an event to calendar is to click "download" on the attachment invite.ics, then open it and only after that does the Calendar app get opened and you can confirm the invitation.


PS: What I mean by rendering the .ics invite is when Outlook or Gmail automatically recognise .ics attachment and display it like on the image below (sorry for the red lines): enter image description here

enter image description here


If it makes any difference, I'm using @sendgrid/mail v6.3.1


Could you please help me to somehow fix my problem? What am I doing wrong?

How to make email clients recognise my .ics files and allow users to accept/decline these invitations in the email client itself without the need to manually download the file and open it?


Solution

  • Okay, so after a lot of trial and error I finally got this working. I hope the code will be helpful to others.

    So, firstly, what I did was send an actual event invite from iCalendar and receive this .ics invite (which actually got rendered in both Outlook and Gmail). I looked at how this file was different from what I was generating and found a curious thing:

    the key to get this working was...

    MAGIC STRINGS

    Yeah, totally random, weird magic strings.

    Below I'm posting the .ics file content that worked for me.

    TOTTALLY-RANDOM-MAGIC-STRING - is a placeholder for a totally random strings like uuids or maybe your organisation emails or anything else.

    The key is: with these strings in the file Outlook and Gmail render the invite correctly, and without them - don't. Weird, but working.

    I wasn't able to find anything meaningful about this in the docs or RFCs, so I guess it's safe for now to call these magic strings.

    The first magic string is [email protected].

    And the second magic string is /TOTTALLY-RANDOM-MAGIC-STRING/principal/.

    BEGIN:VCALENDAR
    PRODID:-//Organisation//Organisation App//EN
    METHOD:REQUEST
    VERSION:2.0
    BEGIN:VEVENT
    DTEND:20210427T160000Z
    ORGANIZER;CN=Organization Name;[email protected]:mailto:[email protected]
    UID:D670DA52-3E7F-4F61-97E2-CB8878954504
    DTSTAMP:20210419T181455Z
    LOCATION:virtual.event.location.com
    DESCRIPTION:description
    URL;VALUE=URI:http://organization.com/invite
    SEQUENCE:0
    SUMMARY:my summary
    LAST-MODIFIED:20210419T181455Z
    DTSTART:20210427T150000Z
    CREATED:20210419T181455Z
    ATTENDEE;CUTYPE=INDIVIDUAL;[email protected]:mailto:[email protected]
    ATTENDEE;CUTYPE=INDIVIDUAL;[email protected]:mailto:[email protected]
    ATTENDEE;CN=Organisation Name;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=CHAIR;[email protected]:/TOTTALLY-RANDOM-MAGIC-STRING/principal/
    END:VEVENT
    END:VCALENDAR
    

    And the code:

      const SendGrid = require("@sendgrid/mail");
    
      const attachment = {
        filename: 'invite.ics',
        name: 'invite.ics',
        content: Buffer.from(data).toString('base64'),
        disposition: 'attachment',
        contentId: uuid(),
        type: 'text/calendar; method=REQUEST',
      };
    
        await SendGrid.send({
          attachments: [attachment],
          templateId,
          from: {
            email: config.emailSender,
            name: config.emailName,
          },
          to: user.email,
          dynamicTemplateData: templateData
       });
    

    I hope this will save some time for people trying to get this .ics stuff working.