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):
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?
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.