如何通过 SendGrid 发送 .ics 日历邀请,以便它在电子邮件客户端中呈现?

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

我正在尝试通过 SendGrid(从 Node 服务器)发送 .ics 日历邀请,以便它在 Outlook 或 Gmail 等客户端中呈现为实际邀请(使用 accept/decline 按钮)而不是作为附件。

我花了几天时间研究这个问题(数十个 Whosebug 问题,RFC-5545, RFC-2446, iCalendar Specification Excerpts, Sendgrid's GitHub issues threads: 1, 2, 3、SendGrid 文档、资源等)。

但是,似乎没有答案(或者我遗漏了什么?)。


到目前为止,我发现附件的 Content-Type 在这里非常重要,尤其是 method=REQUEST 部分。甚至 .

尽管这里有很多关于 SO 的问题,但由于某种原因,大多数问题仍未得到解答。


以下是我设置 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`,
      },
    });

至于 type 属性,我尝试了以下变体:

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'

但是,除了 'text/calendar''application/ics' 之外,没有任何效果(而且它们之间似乎没有任何区别)。

根据 SendGrid 文档,

Content-Type 是保留的 header,因此无法通过 headers 属性 或 smth.[=36= 以某种方式设置它]

disposition: 'inline' 选项也根本不起作用(仅 disposition: 'attachment')。


我生成的 .ics 文件如下所示:

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:john.smith+test1@gmail.com
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:john.smith+test1@gmail.com
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=John Test;X-NUM-GUESTS=0:MAILTO:john.smith+test2@gmail.com
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

该文件完全有效,可以在 iCalendar 中无缝打开。

但是为什么这不会在 Outlook 或 Gmail 中呈现?

目前,将事件添加到日历的唯一方法是单击附件 invite.ics 上的“下载”,然后打开它,然后日历应用程序才会打开,您可以确认邀请.


PS:rendering .ics 邀请的意思是当 Outlook 或 Gmail 自动识别 .ics 附件并如下图所示显示它时(抱歉对于红线):


如果有什么不同,我正在使用 @sendgrid/mail v6.3.1


你能帮我解决一下我的问题吗? 我做错了什么?

如何让电子邮件客户端识别我的 .ics 文件并允许用户在电子邮件客户端本身中 accept/decline 这些邀请,而无需手动下载文件并打开它?

好的,经过反复试验,我终于成功了。希望代码对其他人有帮助。

所以,首先,我所做的是从 iCalendar 发送一个实际的活动邀请并接收这个 .ics 邀请(实际上在 Outlook 和 Gmail 中都呈现)。我查看了这个文件与我生成的文件有何不同,发现了一件奇怪的事情:

实现此功能的关键是...

魔法弦

是的,完全随机的、奇怪的魔法字符串。

下面我发布了对我有用的 .ics 文件内容。

TOTTALLY-RANDOM-MAGIC-STRING - 是一个完全随机字符串的占位符,例如 uuid 或者您的组织电子邮件或其他任何内容。

关键是: 使用文件中的这些字符串,Outlook 和 Gmail 会正确呈现邀请,如果没有它们 - 则不会。奇怪,但有效。

我无法在文档或 RFC 中找到关于此的任何有意义的内容,所以我想现在调用这些 魔术字符串.

是安全的

第一个魔法字符串是TOTTALLY-RANDOM-MAGIC-STRING@imip.me.com

第二个魔法字符串是/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=admin@organisation.com:mailto:TOTTALLY-RANDOM-MAGIC-STRING@imip.me.com
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=my.email1@gmail.com:mailto:my.email1@gmail.com
ATTENDEE;CUTYPE=INDIVIDUAL;EMAIL=my.email2@gmail.com:mailto:my.email2@gmail.com
ATTENDEE;CN=Organisation Name;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=CHAIR;EMAIL=admin@organisation.com:/TOTTALLY-RANDOM-MAGIC-STRING/principal/
END:VEVENT
END:VCALENDAR

代码:

  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
   });

我希望这会为试图让这些 .ics 工作的人们节省一些时间。

对我来说,我只是缺少组织者 mailto 属性,没有 METHOD:REQUEST

从这个 answer 中可以看出,拥有 METHOD:REQUEST 意味着您还需要有一个有效的与会者。这可能是接受的答案有效的原因。

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//sebbo.net//ical-generator//EN
TIMEZONE-ID:Asia/Hong_Kong
X-WR-TIMEZONE:Asia/Hong_Kong
BEGIN:VEVENT
UID:some-uuid
SEQUENCE:0
DTSTAMP:20210626T073540
DTSTART;TZID=Asia/Hong_Kong:20210626T004100
DTEND;TZID=Asia/Hong_Kong:20220625T181200
SUMMARY:Test Event
ORGANIZER;CN="Test Organizer":mailto:somerandomemail@gmail.com
URL;VALUE=URI:http://localhost:3000
STATUS:CONFIRMED
END:VEVENT
END:VCALENDAR

所以,经过大量试验,我终于开始工作了,这里有一个全面的解释和注释,说明我已经开始工作,可能会出现一些问题。

首先,我使用 ics 生成我的日历文件。所以你会像

这样定义你的事件
const event = {
  start: [2018, 5, 30, 6, 30],
  duration: { hours: 1, minutes: 30 },
  title,
  description,
  location: 'Folsom Field, University of Colorado (finish line)', // you can use a link here if it is online
  status: 'CONFIRMED',
  organizer: { name: 'Admin', email: 'Race@BolderBOULDER.com' },
  attendees: [
    { name: 'Adam Gibbons', email: 'adam@example.com', rsvp: true, partstat: 'NEED-ACTIONS', role: 'REQ-PARTICIPANT' },
    { name: 'Brittany Seaton', email: 'brittany@example2.org', rsvp: true, partstat: 'NEED-ACTIONS', role: 'OPT-PARTICIPANT' }
  ],
 method: "REQUEST",
recurrence: "FREQ=WEEKLY;INTERVAL=2",   //weekly
}

您可以在此处添加其他几个键值对,查看ics以获得完整列表。

这里有几点需要注意

  1. 属性 方法定义了与日历对象关联的 iCalendar 对象方法。在 MIME 消息实体中使用时,此 属性 的值必须与 Content-Type“方法”参数值相同。如果指定了“METHOD”属性 或 Content-Type“method”参数,则还必须指定另一个。所以,像这样发送邮件时,它必须匹配内容方法(除非你使用的是动态模板,这并不是完成工作所必需的):
content:[
    {
        type: 'text/calendar; method=REQUEST',
        value
    }
]
  1. 如果您不熟悉循环规则生成器,甚至根本不需要循环,您可以使用 this 作为循环规则生成器。

  2. 确保每个服务员;已指定 rsvprolepartstat

  3. 由于此处指定了组织者的电子邮件,因此您不应将邀请邮件发送给组织者,因为它不会很好地呈现也不会自动添加到他们的日历中,这个问题在 this answer.

因此,如果您还打算将电子邮件发送给组织者以便将其自动添加到他的日历中,您应该考虑将他设为与会者并将贵公司的详细信息设为组织者,例如

{
...
organizer: { name: 'Company Name', email: 'mail@company.com' },
  attendees: [
    { name: 'Admin', email: 'Race@BolderBOULDER.com', rsvp: true, partstat: 'ACCEPTED', role: 'REQ-PARTICIPANT'  },
    { name: 'Adam Gibbons', email: 'adam@example.com', rsvp: true, partstat: 'NEED-ACTIONS', role: 'REQ-PARTICIPANT' },
    { name: 'Brittany Seaton', email: 'brittany@example2.org', rsvp: true, partstat: 'NEED-ACTIONS', role: 'OPT-PARTICIPANT' }
  ]
...
}

因此真正的组织者已被添加为访客并自动将他的 partstat 指定为 accepted。这样,您就可以将电子邮件发送给组织者和来宾,以便自动将其添加到他们的日历中。

那个,继续createEvent从此,

const {value} = ics.createEvent(event)

然后,终于把邮件发出去了

await sgMail.sendMultiple({
    to: attendees,
    subject,
    from: { name, email},
content:[
    {
        type: 'text/calendar; method=REQUEST',
        value // from ics createEvent
    }
],
    attachments: [
        {
            content: Buffer.from(value).toString("base64"),
            type: "application/ics",
            namw: "invite.ics",
            filename: "invite.ics",
            disposition: "attachment",
        },
    ],
})

在这里,我使用 sendMultiple 一次向所有与会者触发事件,并且除了内容之外还有 ics 文件的附件作为某些电子邮件客户端的后备(所以如果需要,用户可以自己单击、打开并添加到日历。

再次提醒,不要将真正的组织者添加到电子邮件的收件人中;因此,如果真正的组织者在与会者列表中,那么您应该 slice 或像我一样 - 将他添加为客人并始终使用公司的详细信息作为标准主持人,然后您可以向所有人发送电子邮件。

如果一切正确,每个人都会收到这封带有 rsvp 的电子邮件和所有漂亮的渲染,具体取决于他们各自的电子邮件客户端,gmail 做得非常棒,然后它会也会自动添加到他们的日历中。