ScriptRunner: Sending outlook invitation as custom mail

Hi, 

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,

Christian

3 answers

1 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 java.io.File


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("my.company.SendOutlookInvitation")


log.setLevel(Level.DEBUG)
log.debug("#####################################")
def last_attachment = issue.attachments.last()
def file_name = last_attachment.filename.toString()
def file_type = last_attachment.mimetype.toString()
def file_id = last_attachment.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")));
    mp.addBodyPart(calendarPart)
    mail.setMultipart(mp)
} else {
    log.debug("FILE_EXISTS: NO")
}


log.debug("#####################################")
true

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

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.  

Matthew,

 

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 https://community.atlassian.com/t5/JIRA-Core-questions/How-can-I-generate-a-UUID-and-assign-to-a-custom-field/qaq-p/324170  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("company.com.SendOutlookInvitation")
 
log.setLevel(Level.WARN)
log.debug("#####################################")

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},
<p></p>
The ${issue.issueType.name} ${issue.key} with summary ${issue.summary} has been scheduled.
<p></p>
Description: $issue.description
<p></p>
<% if (mostRecentComment)
    out << "Last comment: " << mostRecentComment
%>
<p></p>
CustomFieldName: <% out <<
 issue.getCustomFieldValue(
     com.atlassian.jira.component.ComponentAccessor.getCustomFieldManager().getCustomFieldObjectByName("CustomFieldName")
    ) %>
<p></p>
Regards,<br></br>
${issue.reporter?.displayName}

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.

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?

 

Thanks!

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.

 

thanks

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.io.BufferedWriter
import java.io.File
import java.io.FileWriter
import java.io.IOException

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")
log.setLevel(Level.DEBUG)

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 = '''\
BEGIN:VCALENDAR
PRODID:-//Microsoft Corporation//Outlook 14.0 MIMEDIR//EN
VERSION:2.0
METHOD:REQUEST
X-MS-OLK-FORCEINSPECTOROPEN:TRUE
BEGIN:VTIMEZONE
TZID:W. Europe Standard Time
BEGIN:STANDARD
DTSTART:${CREATION_TIME}
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
ATTENDEE;CN="${ORGANIZER}";RSVP=TRUE:mailto:${ORGANIZER_MAIL}
ATTENDEE;CN="${DEPLOYER}":mailto:${DEPLOYER_MAIL}
CLASS:PUBLIC
CREATED:${CREATION_TIME}
DESCRIPTION:${DESCRIPTION}\n
DTEND;TZID="W. Europe Standard Time":${END_TIME}
DTSTAMP:${CREATION_TIME}
DTSTART;TZID="W. Europe Standard Time":${START_TIME}
LAST-MODIFIED:${MOD_TIME}
LOCATION:${LOCATION}
ORGANIZER;CN="${ORGANIZER}";RSVP=TRUE:mailto:${ORGANIZER_MAIL}
PRIORITY:5
SEQUENCE:0
SUMMARY;LANGUAGE=de-ch:${SUMMARY}
TRANSP:OPAQUE
UID:${UID}
X-ALT-DESC;FMTTYPE=text/html:<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//E
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
ODY></HTML>
X-MICROSOFT-CDO-BUSYSTATUS:BUSY
X-MICROSOFT-CDO-IMPORTANCE:1
X-MICROSOFT-DISALLOW-COUNTER:FALSE
X-MS-OLK-APPTSEQTIME:${CREATION_TIME}
X-MS-OLK-AUTOFILLLOCATION:FALSE
X-MS-OLK-CONFTYPE:0
END:VEVENT
END:VCALENDAR'''

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)

log.info("START_TIME: " + sdf2.format(cfDeploymentTimeVal))
Timestamp endTime = new Timestamp(cfDeploymentTimeVal.getTime() + (cfDeploymentDurationVal * 60 * 1000))
log.info("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;
}

log.info("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,
LOCATION : '',
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()) {
log.info("CREATING NEW ATTACHMENT!!!")
attachment.createNewFile();
}

try {
log.info("POPULATING ATTACHMENT ...")
FileWriter fw = new FileWriter(attachment.getAbsoluteFile());
BufferedWriter bw = new BufferedWriter(fw);
bw.write(attachmentContent);
bw.close();
log.info("... POPULATING ATTACHMENT - DONE!!!")
} catch(IOException exc) {
log.error("ATTACHMENT COULD NOT BE CREATED!!!", exc)
}

log.info("ADDING ATTACHMENT ...")
def bean = new CreateAttachmentParamsBean.Builder()
.file(attachment)
.filename(issue.summary + ".ics")
.contentType("text/plain")
.author(user)
.issue(issue)
.build()

attachmentManager.createAttachment(bean)
log.info("... 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 = '''\
...
CREATED:${CREATION_TIME}
...
END:VCALENDAR'''

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

@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

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

and my mail is roni.zeilig@sapiens.com

 

thanks so so much!

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

0 votes

You have full control over the Mail object, so you should be able to do it. The closest thing in the docs is https://scriptrunner.adaptavist.com/latest/jira/builtin-scripts.html#_adding_generated_attachments.

In the linked SO.com 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!

Christian,

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

 

Thanks,

Connor

Suggest an answer

Log in or Join to answer
Community showcase
Teodora [Botron]
Published Thursday in Marketplace Apps

Jira Inferno: The Nine Circles of Jira Administration Hell

If you spend enough time as a Jira admin - whether you are managing a single, mid-sized instance, a large enterprise one or juggling multiple instances at once - you will eventually find yourself in ...

668 views 4 15
Read article

Atlassian User Groups

Connect with like-minded Atlassian users at free events near you!

Find a group

Connect with like-minded Atlassian users at free events near you!

Find my local user group

Unfortunately there are no AUG chapters near you at the moment.

Start an AUG

You're one step closer to meeting fellow Atlassian users at your local meet up. Learn more about AUGs

Groups near you
Atlassian Team Tour

Join us on the Team Tour

We're bringing product updates and pro tips on teamwork to ten cities around the world.

Save your spot