问题描述
我正在尝试通过 SendGrid(来自 Node 服务器)发送 .ics
日历邀请,以便它在 Outlook 或 Gmail 等客户端中呈现为实际邀请(带有接受/拒绝按钮),而不仅仅是作为附件文件。
我花了几天时间研究这个(几十个 Stackoverflow 问题,RFC-5545、RFC-2446、iCalendar Specification Excerpts、Sendgrid 的 GitHub 问题线程:1、2 、3、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'
)。
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
文件并允许用户在电子邮件客户端本身中接受/拒绝这些邀请,而无需手动下载文件并打开它?
解决方法
好的,经过大量的反复试验,我终于得到了这个工作。我希望代码对其他人有帮助。
所以,首先,我所做的是从 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 以获取完整列表。
这里应该注意的几件事
- 属性方法定义了与日历对象关联的 iCalendar 对象方法。当在 MIME 消息实体中使用时,该属性的值必须与 Content-Type“方法”参数值相同。如果指定了“方法”属性或内容类型“方法”参数,则还必须指定另一个。因此,在发送这样的邮件时,它必须匹配 content 方法(除非您使用的是动态模板,这对于完成工作并不是真正必要的):
content:[
{
type: 'text/calendar; method=REQUEST',value
}
]
-
如果您不熟悉循环规则生成器,甚至根本不需要循环,则可以使用 this 来生成循环规则。
-
确保每个服务员; rsvp、role 和 partstat 已指定。
-
由于此处指定了组织者的电子邮件,您不应将邀请邮件发送给组织者,因为它不会很好地呈现也不会自动添加到他们的日历中,该问题在 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 做得非常棒,然后它也会自动添加到他们的日历。