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

Den*_*nko 17 icalendar outlook gmail sendgrid sendgrid-api-v3

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

我花了几天时间研究这个问题(数十个 Stackoverflow 问题、RFC -5545RFC-2446iCalendar规范摘录、Sendgrid 的 GitHub 问题线程:1、2、3 、 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`,
      },
    });
Run Code Online (Sandbox Code Playgroud)

至于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'
Run Code Online (Sandbox Code Playgroud)

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

Content-Type根据 SendGrid 文档,它是一个保留标头,因此不可能通过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

Run Code Online (Sandbox Code Playgroud)

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

但为什么它没有在 Outlook 或 Gmail 中呈现?

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


PS:我所说的rendering邀请.ics是指 Outlook 或 Gmail 自动识别.ics附件并将其显示为如下图所示(抱歉出现红线): 在此输入图像描述

在此输入图像描述


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


您能帮我解决我的问题吗?我究竟做错了什么?

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

Den*_*nko 19

好吧,经过大量的尝试和错误,我终于成功了。我希望代码对其他人有帮助。

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

让这个工作的关键是......

魔法弦

是的,完全随机的、奇怪的魔法弦。

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

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

关键是: 在文件中使用这些字符串,Outlook 和 Gmail 可以正确呈现邀请,如果没有它们,则不会。很奇怪,但工作正常。

我在文档或 RFC 中找不到任何对此有意义的信息,所以我想现在调用这些magic strings是安全的。

第一个神奇的字符串是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
Run Code Online (Sandbox Code Playgroud)

和代码:

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
Run Code Online (Sandbox Code Playgroud)

我希望这能为尝试让这些.ics东西发挥作用的人们节省一些时间。


Abd*_*ipo 9

因此,经过多次试验,我终于成功了,这里有一个全面的解释和注释,说明我如何解决可能出现的问题。

首先,我使用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
}
Run Code Online (Sandbox Code Playgroud)

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

这里有一些你应该注意的事情

  1. 属性方法定义与日历对象关联的 iCalendar 对象方法。当在 MIME 消息实体中使用时,该属性的值必须与 Content-Type“method”参数值相同。如果指定了“METHOD”属性或 Content-Type“method”参数,则还必须指定另一个。因此,在发送这样的邮件时,它必须与内容方法匹配(除非您使用动态模板,这对于完成工作并不是真正必要的):
content:[
    {
        type: 'text/calendar; method=REQUEST',
        value
    }
]
Run Code Online (Sandbox Code Playgroud)
  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' }
  ]
...
}
Run Code Online (Sandbox Code Playgroud)

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

继续前进,createEvent从此,

const {value} = ics.createEvent(event)
Run Code Online (Sandbox Code Playgroud)

然后,最后将电子邮件发送出去

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",
        },
    ],
})
Run Code Online (Sandbox Code Playgroud)

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

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

如果一切都正确完成,每个人都会收到这封电子邮件,其中包含回复和所有精美的渲染,具体取决于他们各自的电子邮件客户端,gmail做得非常出色,然后它也会自动添加到他们的日历中。