Sending Outlook Calendar Invite using Java Mail API


Today I had to implement a functionality related to sending Outlook Calendar invite. The app backend was written in Java. As it turns out the simple task of sending a calendar invite is much more complicated than I expected. You have to construct the message with all the right parameters set else your calendar invite will not behave as you expect. I wasted an hour or two figuring out why RSVP buttons are not coming in the invite. As it turned out it was one of the missing parameters that caused the issue. Also, I wanted to send calendar invite to both outlook and Google calendar.

I couldn’t find a working example on the web and had to spend half my day making it work. So, I am posting the code below that people can use to send calendar invite and hopefully save themselves from the pain and agony.

If you run this code in Spring Boot application then JavaMailSender will be injected by spring. Please make sure to add spring-boot-starter-mail to your class path.

The code snipped below uses Spring Mail support which is based on the Java Mail API.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;

import javax.activation.DataHandler;
import javax.mail.Message;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.util.ByteArrayDataSource;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Properties;
import java.util.UUID;

public class CalendarService {

    private JavaMailSender mailSender;

    public CalendarService(JavaMailSender mailSender) {
        this.mailSender = mailSender;
    }

    public void sendCalendarInvite(String fromEmail, CalendarRequest calendarRequest) throws Exception {
        MimeMessage mimeMessage = mailSender.createMimeMessage();
        mimeMessage.addHeaderLine("method=REQUEST");
        mimeMessage.addHeaderLine("charset=UTF-8");
        mimeMessage.addHeaderLine("component=VEVENT");
        mimeMessage.setFrom(new InternetAddress(fromEmail));
        mimeMessage.addRecipient(Message.RecipientType.TO, new InternetAddress(calendarRequest.getToEmail()));
        mimeMessage.setSubject(calendarRequest.getSubject());
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd HHmmss");
        StringBuilder builder = new StringBuilder();
        builder.append("BEGIN:VCALENDAR\n" +
                "METHOD:REQUEST\n" +
                "PRODID:Microsoft Exchange Server 2010\n" +
                "VERSION:2.0\n" +
                "BEGIN:VTIMEZONE\n" +
                "TZID:Asia/Kolkata\n" +
                "END:VTIMEZONE\n" +
                "BEGIN:VEVENT\n" +
                "ATTENDEE;ROLE=REQ-PARTICIPANT;RSVP=TRUE:MAILTO:" + calendarRequest.getToEmail() + "\n" +
                "ORGANIZER;CN=Foo:MAILTO:" + fromEmail + "\n" +
                "DESCRIPTION;LANGUAGE=en-US:" + calendarRequest.getBody() + "\n" +
                "UID:"+calendarRequest.getUid()+"\n" +
                "SUMMARY;LANGUAGE=en-US:Discussion\n" +
                "DTSTART:" + formatter.format(calendarRequest.getMeetingStartTime()).replace(" ", "T") + "\n" +
                "DTEND:" + formatter.format(calendarRequest.getMeetingEndTime()).replace(" ", "T") + "\n" +
                "CLASS:PUBLIC\n" +
                "PRIORITY:5\n" +
                "DTSTAMP:20200922T105302Z\n" +
                "TRANSP:OPAQUE\n" +
                "STATUS:CONFIRMED\n" +
                "SEQUENCE:$sequenceNumber\n" +
                "LOCATION;LANGUAGE=en-US:Microsoft Teams Meeting\n" +
                "BEGIN:VALARM\n" +
                "DESCRIPTION:REMINDER\n" +
                "TRIGGER;RELATED=START:-PT15M\n" +
                "ACTION:DISPLAY\n" +
                "END:VALARM\n" +
                "END:VEVENT\n" +
                "END:VCALENDAR");

        MimeBodyPart messageBodyPart = new MimeBodyPart();

        messageBodyPart.setHeader("Content-Class", "urn:content-classes:calendarmessage");
        messageBodyPart.setHeader("Content-ID", "calendar_message");
        messageBodyPart.setDataHandler(new DataHandler(
                new ByteArrayDataSource(builder.toString(), "text/calendar;method=REQUEST;name=\"invite.ics\"")));

        MimeMultipart multipart = new MimeMultipart();

        multipart.addBodyPart(messageBodyPart);

        mimeMessage.setContent(multipart);

        System.out.println(builder.toString());

        mailSender.send(mimeMessage);
        System.out.println("Calendar invite sent");

    }

}

Below is the CalendarRequest class.

class CalendarRequest {
    private String uid = UUID.randomUUID().toString();
    private String toEmail;
    private String subject;
    private String body;
    private LocalDateTime meetingStartTime;
    private LocalDateTime meetingEndTime;

    private CalendarRequest(Builder builder) {
        toEmail = builder.toEmail;
        subject = builder.subject;
        body = builder.body;
        meetingStartTime = builder.meetingStartTime;
        meetingEndTime = builder.meetingEndTime;
    }


    public String getUid() {
        return uid;
    }

    public String getToEmail() {
        return toEmail;
    }

    public String getSubject() {
        return subject;
    }

    public String getBody() {
        return body;
    }

    public LocalDateTime getMeetingStartTime() {
        return meetingStartTime;
    }

    public LocalDateTime getMeetingEndTime() {
        return meetingEndTime;
    }

    public static final class Builder {
        private String toEmail;
        private String subject;
        private String body;
        private LocalDateTime meetingStartTime;
        private LocalDateTime meetingEndTime;

        public Builder() {
        }

        public Builder withToEmail(String val) {
            toEmail = val;
            return this;
        }

        public Builder withSubject(String val) {
            subject = val;
            return this;
        }

        public Builder withBody(String val) {
            body = val;
            return this;
        }

        public Builder withMeetingStartTime(LocalDateTime val) {
            meetingStartTime = val;
            return this;
        }

        public Builder withMeetingEndTime(LocalDateTime val) {
            meetingEndTime = val;
            return this;
        }

        public CalendarRequest build() {
            return new CalendarRequest(this);
        }
    }
}

The main method to test the code is below.

public static void main(String[] args) throws Exception {
    JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
    mailSender.setUsername("xxx@example.com");
    mailSender.setPassword("p@ssw0rd");
    Properties properties = new Properties();
    properties.put("mail.smtp.auth", "true");
    properties.put("mail.smtp.starttls.enable", "true");
    properties.put("mail.smtp.host", "smtp-mail.outlook.com");
    properties.put("mail.smtp.port", "587");
    mailSender.setJavaMailProperties(properties);
    CalendarService calendarService = new CalendarService(mailSender);
    calendarService.sendCalendarInvite(
            "xxx@example.com",
            new CalendarRequest.Builder()
                    .withSubject("Test Meeting")
                    .withBody("This is a test event")
                    .withToEmail("yyy@example.com")
                    .withMeetingStartTime(LocalDateTime.now())
                    .withMeetingEndTime(LocalDateTime.now().plusHours(1))
                    .build()
    );
}

Below is the screenshot of the calendar invite in web outlook client.

To attach a document with the calendar invite you can do following.

MimeBodyPart attachment = MimeBodyPart();
String filename = "document.docx";
File file = new File("~/docs/document.docx");
FileDataSource source = FileDataSource(file);
attachment.setDataHandler(new DataHandler(source));
attachment.setFileName(filename);
multipart.addBodyPart(attachment);

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s