Search code examples
ruby-on-railsrubyicalendar

How to get RSVP buttons through Icalendar gem


So I've got the Icalendar gem in my ruby project. I'm attempting to get the RSVP buttons of Yes/No/Maybe on the invite but whenever it gets sent I only get a "Add to Calendar".

Was wondering what else I need:

def make_ical_appointment(start_time, end_time, uid, email_one, email_two)
  ical = Icalendar::Calendar.new
  ical.timezone.tzid = "UTC"

  e = Icalendar::Event.new
  e.dtstart = start_time
  e.dtend = end_time
  e.organizer = %W(mailto:#{email_one} mailto#{email_two})
  e.uid = uid

  ical.add_event(e)
  ical.publish

  mail.attachments['appointment.ics'] = { mime_type: 'application/ics', content: ical.to_ical }
end

I've read that people need to set it to METHOD:REQUEST, but I'm not sure where to do there. I've also read that you need to set attendees, but it seems you can only set attendees if you have an alarm?

Just looking to get it to look like a regular invite.


Solution

  • There's two things you need to do to solve your problem:

    1. Read RFC-2445, which defines the iCal format. It looks like section 4.8.4.1, which discusses the ATTENDEE property, and 4.2.17, which discusses the RSVP parameter, will be of particular interest.

    2. Look at emails and .ics files you've received that display correctly in various email clients.

    The page I linked to in my comment above has three hints.

    The first hint

    I tried adding this property:
    calendar.custom_property("METHOD", "REQUEST").[1]

    From the docs I think that's supposed to be append_custom_property.

    Opening up an invite someone sent me from Google calendar, I found this line:

    METHOD:REQUEST
    

    So that seems legit.

    The second hint

    I would guess that you need to add an ATTENDEE property with RSVP=TRUE and the email set to the same email that Outlook or Yahoo link to their users.[2]

    In the same invite I found this:

    ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=
     TRUE;CN=Firstname Lastname;X-NUM-GUESTS=0:mailto:[email protected]
    

    I didn't read the whole RFC, but I think it breaks down like this:

    • ATTENDEE is the property name.
    • Everything between the first ; and the first : are parameters. Each of them are documented in the RFC, and I don't know if all of them are required, but we can see the RSVP=TRUE parameter there.
    • Everything after the first :, i.e. mailto:[email protected] is the value

    Looking at the source of append_custom_property we see that it checks if value is an Icalendar::Value object, and if not it creates one with Icalendar::Values::Text.new(value). Since we have parameters in addition to a value, let's check out that constructor here. We see that it can take a second argument, which is a params Hash.

    Now, I haven't tested it, but that suggests to me that you can build a line like the above with code something like the following:

    attendee_params = { "CUTYPE"   => "INDIVIDUAL",
                        "ROLE"     => "REQ-PARTICIPANT",
                        "PARTSTAT" => "NEEDS-ACTION",
                        "RSVP"     => "TRUE",
                        "CN"       => "Firstname Lastname",
                        "X-NUM-GUESTS" => "0" }
    
    attendee_value = Icalendar::Values::Text.new("MAILTO:[email protected]", attendee_params)
    ical.append_custom_property("ATTENDEE", attendee_value)
    

    Edit: In Icalendar 2.x it looks like you can also do:

    attendee_value = Icalendar::Values::CalAddress.new("MAILTO:[email protected]", attendee_params)
    ical.append_attendee(attendee_value)
    

    The CalAddress class is a subclass of Uri, which just runs the given value through URI.parse, and append_attendee appears to be a shortcut for append_custom_property("ATTENDEE", ...).

    I'm not sure if all of those parameters are actually required, but you can learn what each of them is by reading the RFC.

    The third hint

    What I had to do to make it work in all mail clients was to send it as a multipart/alternative message with the ical as an alternative view instead of as an attachment.[3]

    Sure enough, doing "Show Original" in Gmail I saw that the invite email I got is a multipart email, with a text/calendar part:

    --047d7b0721581f7baa050a6c3dc0
    Content-Type: text/calendar; charset=UTF-8; method=REQUEST
    Content-Transfer-Encoding: 7bit
    
    BEGIN:VCALENDAR
    PRODID:-//Google Inc//Google Calendar 70.9054//EN
    ...
    

    ...and an application/ics attachment part:

    --047d7b0721581f7bae050a6c3dc2
    Content-Type: application/ics; name="invite.ics"
    Content-Disposition: attachment; filename="invite.ics"
    Content-Transfer-Encoding: base64
    
    QkVHSU46VkNBTEVOREFSDQpQUk9ESUQ6LS8vR29vZ2xlIEluYy8vR29vZ2xlIENhbGVuZGFyIDcw
    ...
    

    The second part you've already got, thanks to mail.attachments. For the first part, you just have to create a new Mail::Part with the correct content_type and add it to mail.parts, which will look something like this:

    ical_part = Mail::Part.new do
      content_type "text/calendar; charset=UTF-8; method=REQUEST"
      body ical.to_ical
    end
    mail.add_part(ical_part)
    

    That's all I've got. Again, I've tested none of this, and I'm not certain it'll fix your problem, but hopefully it gives you a few ideas.

    The most important thing, I think, is to look at the source of emails (if you use Gmail, "Show Original" is under the drop-down menu next to the Reply button) with invites and look at how they're constructed, and likewise look at the .ics attachments and see whether or not they match what you're generating.

    Good luck!


    Judging by the way Icalendar transforms the params hash into iCal parameters, I think you can use symbol keys, too, like so:

    attendee_params = { cutype:   "INDIVIDUAL",
                        role:     "REQ-PARTICIPANT",
                        partstat: "NEEDS-ACTION",
                        rsvp:     "TRUE",
                        cn:       "Firstname Lastname",
                        x_num_guests: "0" }