Come for the products,
stay for the community

The Atlassian Community can help you and your team get more value out of Atlassian products and practices.

Atlassian Community about banner
Community Members
Community Events
Community Groups

ScriptRunner: Sending outlook invitation as custom mail


I want to use the ScriptRunner built-in script send custom email in order to send MS Outlook invitations.

I am already able to create an ICS-Attachment on the issue with a custom scriptrunner script. And I am able to send this attachment with the built-in script but in Outlook it would not be detected as an invitation mail.

In this stackoverflow-entry I found a hint about how to create proper Outlook invitations and I wonder if it would be possible to adopt this somehow.

Kind Regards,


4 answers

1 accepted

1 vote
Answer accepted

Hi Jamie,

thank you for the hint! Kind of helpful was also the recipes area (Validating Attachments/Links in Transitions) of the script runner docs.

I had to fiddle around quite a while but combining the SR docs with another stackoverflow-entry was actually the breakthrough.

Here is what I put in the Condition and Configuration area of the Send a custom email post-function:

import com.atlassian.jira.component.ComponentAccessor
import org.apache.log4j.Logger
import org.apache.log4j.Level


import javax.activation.DataHandler;

import javax.mail.util.ByteArrayDataSource;
import javax.mail.internet.MimeBodyPart
import javax.mail.internet.MimeMultipart
import javax.mail.internet.MimeUtility

def log = Logger.getLogger("")

def last_attachment = issue.attachments.last()
def file_name = last_attachment.filename.toString()
def file_type = last_attachment.mimetype.toString()
def file_id =

log.debug("FILE_NAME: " + file_name)
log.debug("FILE_TYPE: " + file_type)

def attachmentPathManager = ComponentAccessor.getAttachmentPathManager().attachmentPath.toString()
def projectKey = issue.getProjectObject().getKey()
def file_path = attachmentPathManager + "/" + projectKey + "/" + issue.key + "/" + file_id + "/"
log.debug("FILE_PATH: " + file_path)

File f = new File(file_path);
if(f.exists() && !f.isDirectory()) {
    mail.addHeader("method", "REQUEST");
    mail.addHeader("charset", "UTF-8");
    mail.addHeader("component", "VEVENT");
    log.debug("FILE_EXISTS: YES")
    log.debug("FILE_TEXT:" + f.text)
    def mp = new MimeMultipart("alternative")
    def calendarPart = new MimeBodyPart()
    calendarPart.setHeader("Content-Class", "urn:content-  classes:calendarmessage");
    calendarPart.setHeader("Content-ID", "calendar_message");
    calendarPart.setDataHandler(new DataHandler(new ByteArrayDataSource(f.text, "text/calendar")));
} else {
    log.debug("FILE_EXISTS: NO")


I do not have to include any attachments with the general post-function config. This will be handled by this configuration script completely.

The above scripts expects that a proper ics file is already attached to the issue or will be attached during the current transition (in my case another SR script using the attachmentManager). In this second case the mail has to be send at the end of the post-functions so that attaching has already been performed.


Now the mail appears perfectly as an Outlook invitation thumbs-up smile

JamieA Rising Star Dec 16, 2016

Nice job... minor thing, I'm not sure that's the best way to get the attachment file. If the issue has been moved that may not work IIRC.

Thank you! In my case the attachment will be created on the same transition with first post function using another script - so in my case it could not happen that the attachment location would not fit to the issue key.

But I put a warning regarding this handling/precondition and maybe for other cases someone else finds a more robust implementation smile

Christian and Jamie, Thanks for posting this!  This was almost exactly what I needed.  I was able to build on Christian's example above and now have a single block of code that injects a temporary attachment into a scriptrunner "send a custom email" post function to deliver an ics calendar invitation / reminder to an assignee for a task.  



Would you mind sharing your script to inject the temporary ICS attachment?

This code should get you going.

This requires a custom field to hold a UUID.  Jamie explains how to create a UUID in  I added this uuid creation script as a post script on the create transition for my workflow.

Note: This solution is for MS Outlook Client recipients. This will need modifications for the iCal syntax if your intended recipient uses a different mail client

I use this block in the Condition and Configuration section :

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.util.DefaultIssueChangeHolder
import com.atlassian.jira.issue.issuetype.IssueType
import com.atlassian.jira.issue.IssueImpl
import com.atlassian.jira.issue.IssueFieldConstants.*

import org.apache.log4j.Logger
import org.apache.log4j.Level
import java.text.SimpleDateFormat; 
import javax.activation.DataHandler;
import javax.mail.util.ByteArrayDataSource;
import javax.mail.internet.MimeBodyPart
import javax.mail.internet.MimeMultipart
import javax.mail.internet.MimeUtility
def log = Logger.getLogger("")

def optionsManager = ComponentAccessor.getOptionsManager()
def customFieldManager = ComponentAccessor.getCustomFieldManager()

def fieldName1 = "Custom Start DateTime"
def field1 = customFieldManager.getCustomFieldObjectByName(fieldName1).getValue(issue)
log.debug("Custom Start DateTime: " + field1)
def fieldName2 = "Custom End DateTime"
def field2 = customFieldManager.getCustomFieldObjectByName(fieldName2).getValue(issue)
log.debug("Custom End DateTime: " + field1)
def fieldName3 = "Custom UUID"
def field3 = customFieldManager.getCustomFieldObjectByName(fieldName3).getValue(issue)
def fieldName4 = "Change Type"
def field4 = customFieldManager.getCustomFieldObjectByName(fieldName4).getValue(issue)
def busystatus = "BUSY"
if (field4.toString() == "Free") {
busystatus = "FREE"
} def summary = issue.getSummary() def description = issue.getDescription() def issueowner = issue.getAssigneeId() def uuid = field3.toString()
def starttimeValue = field1 def endtimeValue = field2 TimeZone tz = TimeZone.getTimeZone ("Etc/GMT") SimpleDateFormat outsdf = new SimpleDateFormat ("yyyyMMdd'T'HHmmss'Z'"); SimpleDateFormat insdf = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss.S"); Date starttime = insdf.parse("${starttimeValue}"); outsdf.setTimeZone (tz); def UTCstarttime = outsdf.format(starttime) Date endtime = insdf.parse("${endtimeValue}"); outsdf.setTimeZone (tz); def UTCendtime = outsdf.format(endtime) outsdf.setTimeZone (tz); Calendar currentcal = Calendar.getInstance(tz) Date currenttime = currentcal.getTime() def UTCcurrenttime = outsdf.format(currenttime) //change METHOD:PUBLISH to make it automatic //change METHOD:REQUEST to make it provide buttons def iCal = """BEGIN:VCALENDAR PRODID:-//Microsoft Corporation//Outlook 14.0 MIMEDIR//EN VERSION:2.0 METHOD:REQUEST X-MS-OLK-FORCEINSPECTOROPEN:TRUE BEGIN:VEVENT CLASS:PUBLIC CREATED:${UTCcurrenttime} DESCRIPTION:${description} DTEND:${UTCendtime} DTSTAMP:${UTCcurrenttime} DTSTART:${UTCstarttime} LAST-MODIFIED:${UTCcurrenttime} LOCATION:Company Name PRIORITY:5 SEQUENCE:0 SUMMARY;LANGUAGE=en-us:${summary} TRANSP:OPAQUE UID:${uuid} X-MICROSOFT-CDO-BUSYSTATUS:${busystatus} X-MICROSOFT-CDO-IMPORTANCE:1 X-MICROSOFT-DISALLOW-COUNTER:FALSE X-MS-OLK-AUTOFILLLOCATION:FALSE X-MS-OLK-CONFTYPE:0 BEGIN:VALARM TRIGGER:-PT15M ACTION:DISPLAY DESCRIPTION:Reminder END:VALARM END:VEVENT END:VCALENDAR""" mail.addHeader("method", "REQUEST"); mail.addHeader("charset", "UTF-8"); mail.addHeader("component", "VEVENT"); log.debug("FILE_EXISTS: YES") log.debug("FILE_TEXT:" + iCal) def mp = new MimeMultipart("alternative") def calendarPart = new MimeBodyPart() calendarPart.setHeader("Content-Class", "urn:content- classes:calendarmessage"); calendarPart.setHeader("Content-ID", "calendar_message"); calendarPart.setDataHandler(new DataHandler(new ByteArrayDataSource(iCal, "text/calendar"))); mp.addBodyPart(calendarPart) mail.setMultipart(mp) log.debug("#####################################") true

Then in the Email template section, I use this block:

Dear ${issue.assignee?.displayName},
The ${} ${issue.key} with summary ${issue.summary} has been scheduled.
Description: $issue.description
<% if (mostRecentComment)
    out << "Last comment: " << mostRecentComment
CustomFieldName: <% out <<
    ) %>

The Subject template section gets this block:

Scheduled $issue $issue.summary

I select the HTML format and add the appropriate recipients.

The important portion of this is to choose "New" for the Include Attachments section, otherwise your temporary attachment that is built in the Condition and Configuration section will not get added to the email. 

Good Luck!

-edit: We needed the ability to denote Free vs Busy in the appt, so we added a custom field that would toggle this in the ICS file.

Like Ruben Navarro likes this

Wow, thank you! This has been incredible! This is probably the most thorough & effective response I've ever received on Atlassian.


I used your script almost verbatim & it returns no errors. I've substituted my 3 custom fields (Custom Start Date Time - PresentationStart, Custom End Date Time - PresentationEnd, Custom UUID - UUID) and used a post function in a previous transition to populate the UUID field just like Jamie indicated. 


For some reason although the post-function says 'No failures in last -- executions', the email does not send, and the Attachments field in the preview is 'null'


I feel like I'm very close & already appreciate your help tremendously; any ideas on why it may not be sending though?



The attachment field in the preview is null for me as well, but it still attaches to the issue.   

Something that I overlooked early in my trials was the last line of the Condition and Configuration section.  The single line with the word 'true' as in the example is required or the email is not sent because it is the resultant return code of the condition block.  The execution must reach this point or the condition evaluates to false and won't send.

If you really meant 'No failures in last -- executions' and didn't just redact the number of executions, it is possible that your post-function is not being executed.  I try to go back to the basic steps  in troubleshooting, is your workflow published? , are you certain your custom workflow is attached to the correct issue type in your workflow scheme? ...

I would suggest changing the debug level from WARN to DEBUG and then watch your catalina.out file for debugging messages. You might need to add some additional log.debug statements to determine where it is failing in the conditional execution.


You are correct - the problem was that this post function was step #1 in this transition for me. Once I moved it to the last step, it works now.


HUGE shoutout to you Matthew - thanks for the innovative solution!

@Christian Schlaefcke, I really tried making this work- with no success. can you please explain how you created the ICS-Attachment on the issue with a custom scriptrunner script.



Well, it´s been a while but as I am quite good organized I managed to find the script :-) Here you go:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.attachment.CreateAttachmentParamsBean
import com.atlassian.jira.issue.CustomFieldManager


import java.text.SimpleDateFormat
import java.util.Calendar
import java.sql.Timestamp

import java.util.UUID

import org.apache.log4j.Logger
import org.apache.log4j.Level

def log = Logger.getLogger("CreateICSAttachment")

def attachmentManager = ComponentAccessor.getAttachmentManager()

def user = ComponentAccessor.getJiraAuthenticationContext()?.getUser()

def rendererManager = ComponentAccessor.getRendererManager()
def fieldLayoutItem = ComponentAccessor.getFieldLayoutManager().getFieldLayout(issue).getFieldLayoutItem("comment")
def renderer = rendererManager.getRendererForField(fieldLayoutItem)

def templateText = '''\
PRODID:-//Microsoft Corporation//Outlook 14.0 MIMEDIR//EN
TZID:W. Europe Standard Time
DTEND;TZID="W. Europe Standard Time":${END_TIME}
DTSTART;TZID="W. Europe Standard Time":${START_TIME}
N"><HTML><HEAD><META NAME="Generator" CONTENT="MS Exchange Server version
14.02.5004.000"><TITLE></TITLE></HEAD><BODY><!-- Converted from text/rtf
format -->${DESCRIPTION_HTML}<P DIR=LTR><SPAN LANG="de-ch"></SPAN></P></B

def template = new groovy.text.StreamingTemplateEngine().createTemplate(templateText)

Calendar cal = Calendar.getInstance()
SimpleDateFormat sdf1 = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'")
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyyMMdd'T'HHmmss")

def customFieldManager = ComponentAccessor.getComponent(CustomFieldManager)
def cfDeploymentTime = customFieldManager.getCustomFieldObjectByName("Deployment Time")
Timestamp cfDeploymentTimeVal = (Timestamp) issue.getCustomFieldValue(cfDeploymentTime)

def cfDeploymentDuration = customFieldManager.getCustomFieldObjectByName("Deployment Duration")
int cfDeploymentDurationVal = (int) issue.getCustomFieldValue(cfDeploymentDuration)"START_TIME: " + sdf2.format(cfDeploymentTimeVal))
Timestamp endTime = new Timestamp(cfDeploymentTimeVal.getTime() + (cfDeploymentDurationVal * 60 * 1000))"END_TIME: " + sdf2.format(endTime))

String organizer = issue.reporter.getDisplayName()
String organizerMail = issue.reporter.getEmailAddress()

String deployer = issue.assignee.getDisplayName()
String deployerMail = issue.assignee.getEmailAddress()

String description = issue.description = (null) ? '':issue.description

String descriptionHtml = renderer.render(description, null)
descriptionHtml = descriptionHtml.replaceAll("\\n", "\\\\n")
String descriptionHtmlWrapped = ""

int index = 0
int lineLength = 80
while (index < descriptionHtml.length()) {
descriptionHtmlWrapped += descriptionHtml.substring(index, Math.min(index + lineLength,descriptionHtml.length())) + "\n\t";
index += lineLength;
}"DESCRIPTION: " + descriptionHtmlWrapped)

def binding = [
UID : UUID.randomUUID(),
CREATION_TIME : sdf1.format(cal.getTime()),
MOD_TIME : sdf1.format(cal.getTime()),
START_TIME : sdf2.format(cfDeploymentTimeVal),
END_TIME : sdf2.format(endTime),
ORGANIZER : organizer,
ORGANIZER_MAIL : organizerMail,
DEPLOYER : deployer,
DEPLOYER_MAIL : deployerMail,
SUMMARY : issue.summary,
DESCRIPTION : description,
DESCRIPTION_HTML : descriptionHtmlWrapped

String attachmentContent = template.make(binding)

File attachment = new File("<PATH_TO_JIRA_INST_DIR>/temp/sample.ics")

// if file doesnt exists, then create it
if (!attachment.exists()) {"CREATING NEW ATTACHMENT!!!")

FileWriter fw = new FileWriter(attachment.getAbsoluteFile());
BufferedWriter bw = new BufferedWriter(fw);
bw.close();"... POPULATING ATTACHMENT - DONE!!!")
} catch(IOException exc) {
def bean = new CreateAttachmentParamsBean.Builder()
.filename(issue.summary + ".ics")

attachmentManager.createAttachment(bean)"... ADDING ATTACHMENT - DONE!!!")

I hope that you can make it working for you case. Please let me know if you have any further questions.

so so sorry for the late response and thank yo so much for posting this!


what do you mean in your text when it is written: CREATION_TIME? - Im stuck on that ):

Hi @roniz

the CREATION_TIME a placeholder in the template, represented by

def templateText = '''\

The placeholder in the template needs to be mapped with something useful which is happening in the binding section:

def binding = [
CREATION_TIME : sdf1.format(cal.getTime()),

And finally the template will be processed for filling up all placeholders with the content of the binding:

String attachmentContent = template.make(binding)

Next I am writing the attachmentContent to a file on the disk and attach it to the JIRA issue.

I hope I was able to explain the core of the implementation in a comprehensive way.

Best Regards,


@Christian Schlaefckeand- what about the UUID?

how did you create it?

Exactly the same way like the CREATION_TIME - on-the-fly in the template/binding without CF:

def binding = [
UID : UUID.randomUUID(),


thanks! (and thanks for the patience) i will try it!

@Christian Schlaefcke, I really tried, hope it is not out of my scope..

I made a new screen +workflow + etc to match to your definitions in the code (see screenshot attached) but when I try to create a ticket I get the following error:

Error creating issue: Property 'CREATION_TIME' not found


how do I define a property?

Hi @roniz,

where are you located? Do you have a Skype or whatever account? I think the stuff you´re having trouble with is a bit off-topic here. I would like to help and to share my knowledge but this seem to be more efficient in a 1:1 session. What do you think?

Best Regards,


@Christian Schlaefckethat would be super awsom- my skype is Roni Zeilig- Sapiens

and my mail is


thanks so so much!

@Christian Schlaefcke- I sent you a skype invitation from roni Zeilig

@Christian Schlaefcke or @Matthew Harkins , any idea why (after working beautifully for a year or so) now the ICS turns out as "not supported calendar message"?

This is not occurring for me.

I think that the answer to your question is that you may have updated your Outlook Client software or MS Exchange Server. Newer versions of Outlook and Exchange handle the RDATE and RRULE fields of the ics file differently than previous versions.

Also there is a bug in a particular version of MS Exchange 2016 regarding the parsing of VTIMEZONE

See the following MS support bug notice --

In @Christian Schlaefcke 's example he included an RRULE, but no RDATE.  This appears to be supported by the RFC, but I do not know if Outlook is complaining about the RRULE itself (Outlook only supports a subset of possible RRULEs) or the absence of the RDATE field when an RRULE is supplied.


Recurrence every Year on the last Sunday of the 3rd Month   - Counting days backwards could be a problem

See the following MS support article --


See the following patterns are broken in Outlook-- 

Some of these broken recurrence patterns and options are;

  • Exceptions for weekends
    • Move to Friday
    • Move to Monday
    • Move to Nearest Weekday
    • Delete
  • Recur monthly; Every xth and xth xday of the month
    • Example: Every first and third Monday of the month.
  • Recur monthly; xth day counting from the end of the month
    • Not broken but doesn’t count backwards so results in the wrong dates in Outlook.
  • Recur monthly; every xth and xth day of the month
    • Example: Every 3rd and 10th day of the month

Under some circumstances, Meeting Updates or Cancelation messages could also end up broken;

  • When sending an update/cancellation
    • This instance and all previous instances
    • This instance and all future instances
  • When there are exceptions to a supported recurrence and then add an Outlook user for all instances.


Also See the following broken patterns -- 

1. Exceptions for weekends

  • Delete
  • Move to Friday
  • Move to Nearest Weekday
  • Move to Monday

2. Recur monthly: xth day counting from the backward of month
In this case, it is not broken but unable to count backward so display wrong dates in MS Outlook.


Like # people like this

@Matthew Harkins , what a great and highly appreciated answer.

Below you can find the details on the "non-supported" ICS.

Could it be the TRIGGER:-PT15M?

PRODID:-//Microsoft Corporation//Outlook 14.0 MIMEDIR//EN
DESCRIPTION:Keine weiteren Hinweise oder Informationen zur Abwesenheit übermittelt.
SUMMARY;LANGUAGE=de-de:Urlaub - Muerle, Tilman: 22.07.2019 - 05.08.2019 (11.0 Tage) - ABC-123

I could not find any relevant support article referencing the TRIGGER value.

There are just a few differences I note between your "non-supported" ICS and a working ICS from my system.

You have defined the TRANSP twice with opposing values.  Please try your ICS again removing one of the contradictory settings (TRANSP:OPAQUE or TRANSP:TRANSPARENT).


If that does not solve the non-supported ICS problem, I would focus on the other differences:


You might try removing these fields one at a time to see if that resolves the problem.


If none of those changes solve the problem, then I would check your MS Exchange routing.  We have another service that sends similar ICS which recently started appearing as non-supported.  We currently think that this may be due to an older internal MS Exchange server that routes mail to the new upgraded primary MS Exchange server that hosts the destination mailboxes. This other service is configured to use the older MS Exchange server as its smtp endpoint. I am curious if you are also in the process of upgrading your Exchange infrastructure and have your environment configured in a similar way with Jira configured to use an older version of MS Exchange wherein your mailboxes have been migrated to a host with a newer MS Exchange version.  We are operating in this manner for only a short time to allow for incremental migration as services are adjusted in turn from using the older MS Exchange host to using the more recent versioned MS Exchange host.  I expect to see the non-supported ics clear up for us once this additional service is configured to use the newer versioned MS Exchange endpoint.

@Matthew Harkins sorry to jump in on this thread! I've just tried out your code and although a mail gets sent it's just sending as a mail not a meeting invite, is there anything else that needs to be configured or that I haven't done? The only difference from your code is I set the busystatus to be BUSY all the time, would there be any other bits of the script I would need to change?


Eimear :) 

This solution worked well for me but I had some problems with Outlook in Office 365. The email invitations were not being recognised and the email just looked like an ordinary email (no RSVP buttons), and there was no ICS file attached.

I changed one line from

calendarPart.setDataHandler(new DataHandler(new ByteArrayDataSource(iCal, 


calendarPart.setDataHandler(new DataHandler(new ByteArrayDataSource(iCal, "text/calendar;method=REQUEST;name=\"meeting.ics\"")));

 Initial tests look good and both Outlook and Gmail receive valid invitations.


Nice work @Chris Dunne! I can second your fix.  This enabled usable ical notifications for our o365 users and continued to function normally for our on premises users.

1 vote
JamieA Rising Star Dec 14, 2016

You have full control over the Mail object, so you should be able to do it. The closest thing in the docs is

In the linked answer it says outlook ignores the ICS attachment. So you need to create and add a new mail part of type "text/calendar" with the contents of the ics file.

Hi @Jamie Echlin@Matthew Harkins, @Christian Schlaefcke- great post!!

I tried making this work, unfortunately with out much success:

  1. I created a new custom field (type Text Field (single line)) by the name "UUID" and set is as a Custom script post-function:

    import com.atlassian.jira.component.ComponentAccessor

    def customFieldManager = ComponentAccessor.getCustomFieldManager()
    def cf = customFieldManager.getCustomFieldObjectByName("UUID")
    issue.setCustomFieldValue(cf, UUID.randomUUID().toString())

    Is this ok? should I do any thing further? does this field need to be attached to a screen in the workflow? did not quirt understand its purpose..
  2. In the next Status I created a post function "Send a custom email" i took @Matthew Harkins's scripts and in the condition changed  

"Custom Start DateTime"--> "Estimated start time"
"Custom End DateTime" --> "Estimated complition time"
"Custom UUID" --> "UUID" 

all the rest is the same



I did not do any thing with a ICS file- sould I?


and it did not work ):


Any thoughts please?

Many many thanks in advance!


Would you share the script you used to create an ICS file and attach it to the issue?




I like Matthews scripts works well - but how do I set the filename?

In Mac's its becoming "Mail Attachment" and people cant see its an invite

Worksbetter in GMail.





Suggest an answer

Log in or Sign up to answer
Community showcase
Published in Jira

Online AMA this week: Your project management questions answered by Jira Design Lead James Rotanson

We know that great teams require amazing project management chops. It's no surprise that great teams who use Jira have strong project managers, effective workflows, and secrets that bring planning ...

184 views 1 6
Read article

Atlassian Community Events