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

问题描述

我正在尝试通过 SendGrid(来自 Node 服务器)发送 .ics 日历邀请,以便它在 Outlook 或 Gmail 等客户端中呈现为实际邀请(带有接受/拒绝按钮),而不仅仅是作为附件文件

我花了几天时间研究这个(几十个 Stackoverflow 问题,RFC-5545RFC-2446iCalendar Specification Excerpts、Sendgrid 的 GitHub 问题线程:123、SendGrid 文档、来源等)。

然而,这个问题似乎没有答案(或者我错过了什么?)。


到目前为止,我发现附件的 Content-Type 在这里非常重要,尤其是 method=REQUEST 部分。甚至the order of properties in the file makes difference

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


以下是我设置 attachment 对象的方法

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'(它们之间似乎没有任何区别)外,没有任何作用。

Content-Type 是根据 SendGrid 文档的保留标头,因此无法通过 headers 属性或 smth 以某种方式设置它。

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 附件并将其显示如下图所示(对不起,红线):

enter image description here

enter image description here


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


你能帮我以某种方式解决我的问题吗? 我做错了什么?

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

解决方法

好的,经过大量的反复试验,我终于得到了这个工作。我希望代码对其他人有帮助。

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

让这项工作发挥作用的关键是...

魔法弦

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

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

TOTTALLY-RANDOM-MAGIC-STRING - 是一个完全随机的字符串的占位符,比如 uuids 或者你的组织电子邮件或其他任何东西。

关键是:在 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',role: 'OPT-PARTICIPANT' }
  ],method: "REQUEST",recurrence: "FREQ=WEEKLY;INTERVAL=2",//weekly
}

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

这里应该注意的几件事

  1. 属性方法定义了与日历对象关联的 iCalendar 对象方法。当在 MIME 消息实体中使用时,该属性的值必须与 Content-Type“方法”参数值相同。如果指定了“方法”属性或内容类型“方法”参数,则还必须指定另一个。因此,在发送这样的邮件时,它必须匹配 content 方法(除非您使用的是动态模板,这对于完成工作并不是真正必要的):
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',partstat: 'ACCEPTED',role: 'REQ-PARTICIPANT'  },{ name: 'Adam Gibbons',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 做得非常棒,然后它也会自动添加到他们的日历。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...