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

16
我正在尝试通过SendGrid(来自Node服务器)发送.ics日历邀请,以便在Outlook或Gmail等客户端中呈现为实际邀请(带有接受/拒绝按钮),而不仅仅是作为附件文件。
我已经花了好几天的时间研究这个问题(阅读了数十篇Stackoverflow问题,RFC-5545RFC-2446iCalendar Specification Excerpts,Sendgrid的GitHub问题线程:123,SendGrid文档,来源等)。
然而,似乎就是没有一个答案(或者我漏掉了什么?)。
到目前为止,我发现附件的Content-Type非常重要,特别是method=REQUEST部分。甚至文件中属性的顺序也很重要。尽管在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"之外,没有任何东西起作用(它们之间似乎没有任何区别)。
根据SendGrid文档,"Content-Type"是一个保留头,因此无法通过"headers"属性或其他方式设置它。
"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:我所说的“渲染”.ics邀请是指Outlook或Gmail自动识别.ics附件,并像下面的图像一样显示它(很抱歉,红线): enter image description here

enter image description here


如果有任何区别的话,我正在使用@sendgrid/mailv6.3.1


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

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

4个回答

18

好的,经过很多的试验和错误,我终于让它工作了。希望这段代码对其他人有所帮助。

首先,我发送了一个实际的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文件运行起来的人节省一些时间。


哇,我不知道你是怎么找到的。但是谢谢!我已经苦苦挣扎了很久。 - Brian Weinreich
我仍然无法在Outlook中显示这个ICS小部件 =( - Armalong
我尝试了你的代码片段并且也写了自己的。Gmail 正确地显示小部件,但 Outlook 却没有。 在我看来,这不仅仅与 ICS 文件及其头文件有关,还与邮件消息头本身有关。 例如,如果您在 Outlook 客户端上设置了带有 example1@outlook.com 域的帐户,并且在 Outlook 上设置了另一个带有类似 example2@email.com 的私人域的帐户。并且您向这些地址发送了两封相同的电子邮件并在 Outlook 中打开它们。您将会看到 example1@outlook.com 的小部件,而 example2@email.com 则不会显示。 - Armalong
如果您检查这些电子邮件源,您会发现,Outlook.com 域中的邮件已经添加了特殊的标题,如“X-MS-*”,以便于适当地显示小部件。而在 email.com 域中并没有添加此类标题,因此小部件无法显示。我并没有想出这个逻辑,但是我提到的是事实。我相信 Gmail 也会执行相同的操作,并在电子邮件消息中添加特殊的标题,以便 ICS 文件能够被捕获和正确显示。 - Armalong
除了UID之外,我不需要任何魔法字符串,但我将这些答案组合在一起,以便邀请提示出现。ATTENDEE部分是关键。 - dustbuster

8

经过多次尝试,我终于使它工作了。这里是对我如何使用ics生成我的日历文件的全面说明和注意事项,包括可能出现的问题。

首先,我使用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. property方法定义了与日历对象相关联的iCalendar对象方法。当在MIME消息实体中使用时,此属性的值必须与Content-Type“method”参数值相同。如果指定了“METHOD”属性或Content-Type“method”参数,则另一个属性也必须被指定。因此,它在发送邮件时必须与内容方法匹配(除非您正在使用动态模板,这并不是真正完成工作所必需的):
content:[
    {
        type: 'text/calendar; method=REQUEST',
        value
    }
]
  1. 如果您不熟悉重复规则生成器,或者根本不需要重复性,请使用此工具

  2. 确保为每个出席者指定rsvprolepartstat

  3. 由于组织者的电子邮件已在此处指定,因此您不应将邀请邮件发送给组织者,因为这样做可能无法良好显示,也无法自动添加到他们的日历中,该问题在此答案中有详细解释。

因此,如果您计划向组织者发送电子邮件以便能够自动添加到他的日历中,您应该考虑将他作为出席者,并将您公司的详细信息作为组织者。例如:

{
...
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)非常出色,然后还会自动添加到他们的日历中。

2

对我而言,我只是缺少了组织者的mailto属性,没有METHOD:REQUEST

从这个答案来看,它解释说,如果有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

将附加为"type => text/calendar; method=request"是使邀请对我们起作用的原因! - dustbuster

2
尝试在.ics的mailto和SendGrid的from中使用相同的电子邮件地址。

请查看下面如何使用info@tinywhitebird.com:

js部分

const message = {
    to: email,
    from: "info@tinywhitebird.com",
    subject: emailSubject,
    text: textContent
}
    
await SendGrid.send({
    ...message,
    html: htmlContent,
    attachments: [attachment]
})

.ics part

ORGANIZER;CN=info@tinywhitebird.com:mailto:info@tinywhitebird.com

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接