Google Provisioning API no longer allowing restore(unsuspend) of user - google-provisioning-api

Anybody else seeing this?
There appears to have been some changes to the Google provisioning api for multi-domains. I have long running code that could restore a suspended user that has stopped working. I use Python and 2.0.17 of the Python GData libraries and the UpdateUser method to do this. I have also noted that RetrieveUser in the same library is no longer returning the first and last names of suspended users. I have filed an issue at Google apps-api-issues, please star if you are seeing this.
http://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=3281

This is a simple example that will walk through the problem. Note that user_entry object returned from a RetrieveUser() on a suspended user will not have a property value for either first name or last name. The modified user_entry object is passed to UpdateUser() which does not allow the missing values for first and last names.
#!/usr/bin/python
import sys
import gdata.apps.multidomain.client
if len(sys.argv) < 4:
print "\nUsage:"
print sys.argv[0], "admin_email admin_password user_email\n"
sys.exit(0)
admin = sys.argv[1]
password = sys.argv[2]
email = sys.argv[3]
domain = ""
if '#' in admin:
admin_name,domain = admin.split('#', 1)
else:
print "Specify full email address of administrator.\n"
print "\nUsage:"
print sys.argv[0], "admin_email admin_password user_email\n"
sys.exit(0)
if '#' not in email:
print "Specify full email address of user.\n"
print "\nUsage:"
print sys.argv[0], "admin_email admin_password user_email\n"
sys.exit(0)
md_client = gdata.apps.multidomain.client.MultiDomainProvisioningClient(
domain=domain)
md_client.ClientLogin(email=admin, password=password, source='MDPROVISIONING')
print "Retrieve user: %s\n" %(email)
user_entry = md_client.RetrieveUser(email)
print user_entry
print ('\nRetrieve results: email: %s, suspended: %s,'
' first name: %s, last name: %s\n'
%(user_entry.email,user_entry.suspended,
user_entry.first_name,user_entry.last_name))
print "Update user (suspend): %s\n" %(email)
user_entry.suspended = 'true'
updated_user_entry = md_client.UpdateUser(email, user_entry)
print updated_user_entry
print ('\nSuspend results: email: %s, suspended: %s,'
' first name: %s, last name: %s\n'
%(updated_user_entry.email,updated_user_entry.suspended,
updated_user_entry.first_name,updated_user_entry.last_name))
print "Retrieve user: %s\n" %(email)
user_entry = md_client.RetrieveUser(email)
print user_entry
print ('\nRetrieve results: email: %s, suspended: %s,'
' first name: %s, last name: %s\n'
%(user_entry.email,user_entry.suspended,
user_entry.first_name,user_entry.last_name))
print "Update user (restore): %s\n" %(email)
user_entry.suspended = 'false'
updated_user_entry = md_client.UpdateUser(email, user_entry)
print updated_user_entry
print ('\nRestore results: email: %s, suspended: %s,'
' first name: %s, last name: %s\n'
%(updated_user_entry.email,updated_user_entry.suspended,
updated_user_entry.first_name,updated_user_entry.last_name))

Thanks user1803418, that explains why restoring a user works with GAM and not your code. The lack of firstName and lastName in the RetrieveUser API call is definitely an issue on Google's end that they'll need to fix. However, I'd describe the unsuspend issue as a client library deficiency that's excaberated by the firstName/lastName issue.
The API requires only attributes that are being updated be submitted in the update user API call. There's no need to specify the user's firstName, lastName and other object details. This means retrieving the user in order to perform the update simply makes the update process take longer, only one API call is really necessary.
GAM uses it's own implementation of the multidomain API calls which I wrote before the multidomain/client.py was released. GAM's implementation does not require that all user attributes be set in order to perform the update. You can see this by looking at:
http://code.google.com/p/google-apps-manager/source/browse/trunk/gdata/apps/multidomain/service.py#69
only the attributes that are set when calling UpdateUser() are sent to Google. You can see this for yourself by installing GAM and creating a file named debug.gam before unsuspending the user with the GAM command:
gam update user suspended off
the XML request to Google will be minimal, showing only a few fields including the suspended status of the user. firstName and lastName will not be included in the request.
So I think you have 3 options here:
1) wait for a fix from Google for the firstName/lastName issue. This should resolve the unsuspend issue also.
2) rewrite the multidomain/client.py UpdateUser() function so that it does not require all user attributes to be set in order to perform the update.
3) switch your code to use GAM's custom multidomain/service.py library since it supports updating only provided attributes for a user.

I hope that Google fixes the missing names soon.
Modifying UpdateUser in the multidomain client
(gdata.apps.multidomain.client) seems to keep things going:
def update_user(self, email, user_entry, **kwargs):
user_entry = gdata.apps.multidomain.data.UserEntry(
email=email,first_name=user_entry.first_name,
last_name=user_entry.last_name,password=user_entry.password,
change_password=user_entry.change_password_at_next_login,
suspended=user_entry.suspended, is_admin=user_entry.is_admin,
ip_whitelisted=user_entry.ip_whitelisted, quota=user_entry.quota)
return self.update(user_entry,
uri=self.MakeMultidomainUserProvisioningUri(email),
**kwargs)

Related

AWS CDK DocDB::DBCluster fails with 'not a valid password'

I am trying to use AWS CKD (JAVA) to create a DocumentDB instance.
This works with a "simple" plaintext password, but fails when I try to use a DatabaseSecret and a password stored in Secrets Manager.
The error I get is this:
1:44:42 PM | CREATE_FAILED | AWS::DocDB::DBCluster | ApiDocDb15EB2C21
The parameter MasterUserPassword is not a valid password. Only printable ASCII characters besides '/', '#', '"', ' ' may
be used. (Service: AmazonRDS; Status Code: 400; Error Code: InvalidParameterValue; Request ID: c786d247-8ff2-4f30-9a8a-5
065fc89d3d1; Proxy: null)
which is clear enough, but it continues to happen, even if I set the password to something such as simplepassword - so I am now somewhat confused as to what am I supposed to fix now.
Here is the code, mostly adapted from the DocDB documentation:
String id = String.format(DOCDB_PASSWORD_ID);
return DatabaseSecret.Builder.create(scope, id)
.secretName(store.getSsmSecretName())
.encryptionKey(passwordKey)
.username(store.getAdminUser())
.build();
where the ssmSecretName is the name of the secret in SecretManager:
└─( aws secretsmanager get-secret-value --secret-id api-db-admin-pwd
ARN: arn:aws:secretsmanager:us-west-2:<ACCT>:secret:api-db-admin-pwd-HHxpFf
Name: api-db-admin-pwd
SecretString: '{"api-db-admin-pwd":"simplepassword"}'
This is the code used to build the DbCluster:
DatabaseCluster dbCluster = DatabaseCluster.Builder.create(scope, id)
.dbClusterName(properties.getDbName())
.masterUser(Login.builder()
.username(properties.getAdminUser())
.kmsKey(passwordKey)
.password(masterPassword.getSecretValue())
.build())
.vpc(vpc)
.vpcSubnets(ISOLATED_SUBNETS)
.securityGroup(dbSecurityGroup)
.instanceType(InstanceType.of(InstanceClass.MEMORY5, InstanceSize.LARGE))
.instances(properties.getReplicas())
.storageEncrypted(true)
.build();
The question I have is: should I use a DatabaseSecret? or just retrieve the password from SM and be done with it?
A sub-question then: what is one supposed to use the DatabaseSecret for then?
(NOTE -- this is the same class, almost, as in the rds package; but here I am using the docdb package)
Thanks for any suggestion!
Turns out that the DatabaseSecret creates a key/value pair as the secret:
{
"username": <value of username()>,
"password": <generated>
}
However, the call to Login.password() completely ingnores this, and treats the whole JSON body as the password (so the " double quotes trip it).
The trick is to use DatabaseSecret.secretValueFromJson("password") in the call to Login.password() and it works just fine.
This is (incidentally) inconsistent with the behavior of rds.DatabaseCluster and the rds.Credentials class behavior (who take a JSON SecretValue and parse it correctly for the "password" field).
Leaving it here in case others stumble on this, as there really is NO information out there.

How to debug RocketChat error-not-allowed (trying to invite and add/remove owners)

We're using RocketChat via a Docker image rocketchat/rocket.chat:0.68.4 and the Ruby rocketchat gem.
There's already some working functionality to update a channel's attributes:
# RocketChatService is a wrapper class for a RocketChat::Session object with authentication as admin
channels = RocketChatService.channels
channels.set_attr(name: id, topic: escape_nil(title)) if title_changed?
channels.set_attr(name: id, description: escape_nil(description)) if description_changed?
channels.set_attr(name: id, custom_fields: { project_id: project_id }) if project_id && project_id_was.nil?
But now we also need to add new users to a room, make them owner, or degrade previous owners. The following code works under certain circumstances (which I'm afraid are to complex to be presented here), but sometimes causes errors:
# idempotent
channels.invite(name: id, username: creator_id)
# TODO: already an owner
channels.add_owner(name: id, username: creator_id)
channels.remove_owner(name: id, username: creator_id_was)
The problem is that these error messages aren't very informative:
"exception"=>"Not allowed [error-not-allowed]"
There's no log file in RocketChat and there's nothing written to stdout when this happens. That brings me to my question: How can I debug an error message like the one above?

Active model OTP not generating new secret key after 30 seconds sleep

I am trying implement active_model_otp on a sample rails application.
My user model has name and email columns. After following the necessary steps from the documentation I first created a user as follows on rails console:
User.create(name: "Abc", email: "example#email.com")
This created a record as follows:
<User id: 1, name: "Abc", email: "example#email.com", created_at: "2017-04-28 07:12:25", updated_at: "2017-04-28 07:12:25", otp_secret_key: "lol6rrtqppy46xfs">
Then I assigned:
user = User.last and
user.otp_secret_key gave the otp that was generated which is
=>"lol6rrtqppy46xfs"
Then I ran
sleep(2)
According to given documentation it supposed to generate a new otp after 2 seconds. But when I gave: user.otp_secret_key again in the console after 2 seconds it is returning the same old otp.
=>"lol6rrtqppy46xfs"
What am I missing ?
Twilio developer evangelist here.
The otp_secret_key is the string that is used to generate the actual OTP code that you send to the user. It is the secret that is shared to the user (via the QR code) and so it needs to match on both the user's auth app as well as your server.
The generated OTP should change every 30 seconds. As far as I can see it uses the default interval of the ROTP gem, which is 30 seconds.
The method to get the OTP is otp_code. So to check it's working, try:
user = User.last
puts user.otp_code
sleep(30)
puts user.otp_code
Let me know if that helps at all.

How can I mass delete comments in JIRA?

We have several JIRA issues which have over 1000 duplicated, bogus, spam-like comments. How can we quickly delete them?
Background:
We disabled a user in active directory (Exchange) but not JIRA, so JIRA kept trying to email them updates. The email server gave a bounce-back message, and JIRA dutifully logged it to the task, which caused it to send another update, and a feedback loop was born.
The messages have this format:
Delivery has failed to these recipients or groups:
mail#example.com<mail#example.com>
The e-mail address you entered couldn't be found. Please check the recipient's e-mail address and try to resend the message. If the problem continues, please contact your helpdesk.
Diagnostic information for administrators:
Generating server: emailserver.example.com
user#example.com
#550 5.1.1 RESOLVER.ADR.RecipNotFound; not found ##
Original message headers:
Received: from jiraserver.example.com (10.0.0.999) by emailserver.example.com (10.0.0.999)
with Microsoft SMTP Server id nn.n.nnn.n; Mon, 13 Jun 2016 15:57:04 -0500
Date: Mon, 13 Jun 2016 15:57:03 -0500
Our research did not discover an easy way without using purchased plug-ins such as Script Runner or "hacking" the database, which we wanted to avoid.
Note:
We came up with a solution and are posting here to share.
I created a python script to remove all comments for a specific Jira issue.
It uses the API from Jira.
'''
This script removes all comments from a specified jira issue
Please provide Jira-Issue-Key/Id, Jira-URL, Username, PAssword in the variables below.
'''
import sys
import json
import requests
import urllib3
# Jira Issue Key or Id where comments are deleted from
JIRA_ISSUE_KEY = 'TST-123'
# URL to Jira
URL_JIRA = 'https://jira.contoso.com'
# Username with enough rights to delete comments
JIRA_USERNAME = 'admin'
# Password to Jira User
JIRA_PASSWORD = 'S9ev6ZpQ4sy2VFH2_bjKKQAYRUlDfW7ujNnrIq9Lbn5w'
''' ----- ----- Do not change anything below ----- ----- '''
# Ignore SSL problem (certificate) - self signed
urllib3.disable_warnings()
# get issue comments:
# https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-issue-issueIdOrKey-comment-get
URL_GET_COMMENT = '{0}/rest/api/latest/issue/{1}/comment'.format(URL_JIRA, JIRA_ISSUE_KEY)
# delete issue comment:
# https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-issue-issueIdOrKey-comment-id-delete
URL_DELETE_COMMENT = '{0}/rest/api/2/issue/{1}/comment/{2}'
def user_yesno():
''' Asks user for input yes or no, responds with boolean '''
allowed_response_yes = {'yes', 'y'}
allowed_response_no = {'no', 'n'}
user_response = input().lower()
if user_response in allowed_response_yes:
return True
elif user_response in allowed_response_no:
return False
else:
sys.stdout.write("Please respond with 'yes' or 'no'")
return False
# get jira comments
RESPONSE = requests.get(URL_GET_COMMENT, verify=False, auth=(JIRA_USERNAME, JIRA_PASSWORD))
# check if http response is OK (200)
if RESPONSE.status_code != 200:
print('Exit-101: Could not connect to api [HTTP-Error: {0}]'.format(RESPONSE.status_code))
sys.exit(101)
# parse response to json
JSON_RESPONSE = json.loads(RESPONSE.text)
# get user confirmation to delete all comments for issue
print('You want to delete {0} comments for issue {1}? (yes/no)' \
.format(len(JSON_RESPONSE['comments']), JIRA_ISSUE_KEY))
if user_yesno():
for jira_comment in JSON_RESPONSE['comments']:
print('Deleting Jira comment {0}'.format(jira_comment['id']))
# send delete request
RESPONSE = requests.delete(
URL_DELETE_COMMENT.format(URL_JIRA, JIRA_ISSUE_KEY, jira_comment['id']),
verify=False, auth=(JIRA_USERNAME, JIRA_PASSWORD))
# check if http response is No Content (204)
if RESPONSE.status_code != 204:
print('Exit-102: Could not connect to api [HTTP-Error: {0}; {1}]' \
.format(RESPONSE.status_code, RESPONSE.text))
sys.exit(102)
else:
print('User abort script...')
source control: https://gist.github.com/fty4/151ee7070f2a3f9da2cfa9b1ee1c132d
Use the JIRA REST API through the Chrome JavaScript Console.
Background:
We didn't want to write a full application for what we hope is an isolated occurrence. We originally planned to use PowerShell's Invoke-WebRequest. However, authentication proved to be a challenge. The API supports Basic Authentication, though it's only recommended when using SSL, which we weren't using for our internal server. Also, our initial tests resulted in 401 errors (perhaps due to a bug).
However, the API also supports cookie-based authentication, so as long as you are generating the request from a browser which has a valid JIRA session, it just works. We chose that method.
Solution details:
First, find and review the relevant comment and issue IDs:
SELECT * FROM jira..jiraaction WHERE actiontype = 'comment' AND actionbody LIKE '%RESOLVER.ADR.RecipNotFound%';
This might be a slow query depending on the size of your JIRA data. It seems to be indexed on the issueid, so if you know that, specify it. Also, add other criteria to this query so that it only represents the comments you wish to delete.
The solution below is written for comments on a single issue, but with some additional JavaScript could be expanded to support multiple issues.
We need the list of comment IDs for use in the Chrome JavaScript console. A useful format is a comma-delimited list of strings, which you can create as follows:
SELECT '"' + CONVERT(VARCHAR(50),ID) + '", ' FROM jira..jiraaction WHERE actiontype = 'comment' AND actionbody LIKE '%RESOLVER.ADR.RecipNotFound%' AND issueid = #issueid FOR XML PATH('')
(This is not necessarily the best way to concatenate strings in SQL, but it's simple and works for this purpose.)
Now, open a new browser session and authenticate to your JIRA instance. We used Chrome, but any browser with a JavaScript console should do.
Take the string produced by that query and drop it in the JavaScript console inside of a statement like this:
CommentIDs = [StringFromSQL];
You will need to trim the trailing comma manually (or adjust the above query to do so for you). It will look like this:
CommentIDs = ["1234", "2345"];
When you run that command, you will have created a JavaScript array with all of those comment IDs.
Now we arrive at the meat of the technique. We will loop over the contents of that array and make a new AJAX call to the REST API using XMLHttpRequest (often abbreviated XHR). (There is also a jQuery option.)
for (let s of CommentIDs) {let r = new XMLHttpRequest; r.open("DELETE","http://jira.example.com/rest/api/2/issue/11111/comment/"+s,true); r.send();}
You must replace "11111" with the relevant issue ID. You can repeat this for multiple issue IDs, or you can build a multi-dimensional array and a fancier loop.
This is not elegant. It doesn't have any error handling, but you can monitor the progress using the Chrome JavaScript API.
I would use a jira-python script or a ScriptRunner groovy script. Even for a one-off bulk update, because it is easier to test and requires no database access.
Glad it worked for you though!
We solved this problem, which occurs from time to time, with ScriptRunner and a Groovy script:
// this script takes some time, when executing it in console, it takes a long time to repsonse, and then the console retunrs "null"
// - but it kepps running in the backgorund, give it some time - at least 1 second per comment and attachment to delete.
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.issue.comments.Comment
import com.atlassian.jira.issue.comments.CommentManager
import com.atlassian.jira.issue.attachment.Attachment
import com.atlassian.jira.issue.managers.DefaultAttachmentManager
import com.atlassian.jira.issue.AttachmentManager
import org.apache.log4j.Logger
import org.apache.log4j.Level
log.setLevel(Level.DEBUG)
// NRS-1959
def issueKeys = ['XS-8071', 'XS-8060', 'XS-8065', 'XRFS-26', 'NRNM-45']
def deleted_attachments = 0
def deleted_comments = 0
IssueManager issueManager = ComponentAccessor.issueManager
CommentManager commentManager = ComponentAccessor.commentManager
AttachmentManager attachmentManager = ComponentAccessor.attachmentManager
issueKeys.each{ issueKey ->
MutableIssue issue = issueManager.getIssueObject(issueKey)
List<Comment> comments = commentManager.getComments(issue)
comments.each {comment ->
if (comment.body.contains('550 5.1.1 The email account that you tried to reach does not exist')) {
log.info issueKey + " DELETE comment:"
//log.debug comment.body
commentManager.delete(comment)
deleted_comments++
} else {
log.info issueKey + " KEEP comment:"
log.debug comment.body
}
}
List<Attachment> attachments = attachmentManager.getAttachments(issue)
attachments.each {attachment ->
if (attachment.filename.equals('icon.png')) {
log.info issueKey + " DELETE attachment " + attachment.filename
attachmentManager.deleteAttachment(attachment)
deleted_attachments++
} else {
log.info issueKey + " KEEP attachment " + attachment.filename
}
}
}
log.info "${deleted_comments} deleted comments, and ${deleted_attachments} deleted attachments"
return "${deleted_comments} deleted comments, and ${deleted_attachments} deleted attachments"

Jira for bug tracking and customer support?

We are thinking of using Jira for bug tracking and to integrate it with Git to connect bug fixes with version handling.
Do you recommend Jira also for customer support or should we find another system like for example Zendesk for that purpose? I know that it is possible somehow to integrate for example Hipchat with Jira to enable chat functionality with customers but is Jira too complex for Customer Service to handle? What is your experience?
We use Jira for customer support, but we found that Jira is missing many must-have features that are needed for this. that's why we make many changes.
All and all, we are very happy with our choice, and we managed to save a lot of money by using Jira instead of other solutions.
Here are the major changes that we made, this will show you what is missing, while on the other hand show you that with a little bit of programming, Jira can do anything :)
Note: The scripts writen below should be attach to a workflow transition. The scripts are written using Jython, so it needs to be installed to use it.
Create issues by email
Jira only sends emails to Jira users. Since we didn't want to create a user for every person that addressed the support, we used anonymous users instead, and used scripts to send them email.
First, set Jira to create issues from emails. Than, use Script Runner pluging to save customer's email and names to custom field. . code:
from com.atlassian.jira import ComponentManager
import re
cfm = ComponentManager.getInstance().getCustomFieldManager()
# read issue description
description = issue.getDescription()
if (description is not None) and ('Created via e-mail received from' in description):
# extract email and name:
if ('<' in description) and ('>' in description):
# pattern [Created via e-mail received from: name <email#company.com>]
# split it to a list
description_list = re.split('<|>|:',description)
list_length = len(description_list)
for index in range(list_length-1, -1, -1):
if '#' in description_list[index]:
customer_email = description_list[index]
customer_name = description_list[index - 1]
break
else:
# pattern [Created via e-mail received from: email#company.com]
customer_name = "Sir or Madam"
# split it to a list
description_list = re.split(': |]',description)
list_length = len(description_list)
for index in range(list_length-1, -1, -1):
if '#' in description_list[index]:
customer_email = description_list[index]
break
# if the name isn't in the right form, switch it's places:
if (customer_name[0] == '"') and (customer_name[-1] == '"') and (',' in customer_name):
customer_name = customer_name[1:-1]
i = customer_name.index(',')
customer_name = customer_name[i+2:]+" "+customer_name[:i]
# insert data to issue fields
issue.setCustomFieldValue(cfm.getCustomFieldObject("customfield_10401"),customer_email)
issue.setCustomFieldValue(cfm.getCustomFieldObject("customfield_10108"),customer_name)
Send customer issue created notification
Send the mail using the following script:
import smtplib,email
from smtplib import SMTP
from email.MIMEMultipart import MIMEMultipart
from email.MIMEBase import MIMEBase
from email.MIMEText import MIMEText
from email import Encoders
import os
import re
from com.atlassian.jira import ComponentManager
customFieldManager = ComponentManager.getInstance().getCustomFieldManager()
cfm = ComponentManager.getInstance().getCustomFieldManager()
# read needed fields from the issue
key = issue.getKey()
#status = issue.getStatusObject().name
summary = issue.getSummary()
project = issue.getProjectObject().name
# read customer email address
toAddr = issue.getCustomFieldValue(cfm.getCustomFieldObject("customfield_10401"))
# send mail only if a valid email was entered
if (toAddr is not None) and (re.match('[A-Za-z0-9._%+-]+#(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,4}',toAddr)):
# read customer name
customerName = issue.getCustomFieldValue(cfm.getCustomFieldObject("customfield_10108"))
# read template from the disk
template_file = 'new_case.template'
f = open(template_file, 'r')
htmlBody = ""
for line in f:
line = line.replace('$$CUSTOMER_NAME',customerName)
line = line.replace('$$KEY',key)
line = line.replace('$$PROJECT',project)
line = line.replace('$$SUMMARY',summary)
htmlBody += line + '<BR>'
smtpserver = 'smtpserver.com'
to = [toAddr]
fromAddr = 'jira#email.com'
subject = "["+key+"] Thank You for Contacting Support team"
mail_user = 'jira#email.com'
mail_password = 'password'
# create html email
html = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" '
html +='"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml">'
html +='<body style="font-size:12px;font-family:Verdana">'
html +='<p align="center"><img src="http://path/to/company_logo.jpg" alt="logo"></p> '
html +='<p>'+htmlBody+'</p>'
html +='</body></html>'
emailMsg = email.MIMEMultipart.MIMEMultipart('alternative')
emailMsg['Subject'] = subject
emailMsg['From'] = fromAddr
emailMsg['To'] = ', '.join(to)
emailMsg.attach(email.mime.text.MIMEText(html,'html'))
# Send the email
s = SMTP(smtpserver) # ip or domain name of smtp server
s.login(mail_user, mail_password)
s.sendmail(fromAddr, [to], emailMsg.as_string())
s.quit()
# add sent mail to comments
cm = ComponentManager.getInstance().getCommentManager()
email_body = htmlBody.replace('<BR>','\n')
cm.create(issue,'anonymous','Email was sent to the customer ; Subject: '+subject+'\n'+email_body,False)
content of new_case.template:
Dear $$CUSTOMER_NAME,
Thank you for contacting support team.
We will address your case as soon as possible and respond with a solution very quickly.
Issue key $$KEY has been created as a reference for future correspondence.
If you need urgent support please refer to our Frequently Asked Questions page at http://www.example.com/faq.
Thank you,
Support Team
Issue key: $$KEY
Issue subject: $$PROJECT
Issue summary: $$SUMMARY
Issue reminder - open for 24/36/48 hours notifications
Created a custom field called "Open since" - a 'Date Time' field to hold the time the issue has been opened.
Created a custom field called "Notification" - a read only text field.
Using the Script Runner pluging , I've created a post-function and placed it on every transition going to the 'Open' status. This is to keep the issue opening time.
the code:
from com.atlassian.jira import ComponentManager
from datetime import datetime
opend_since_field = "customfield_10001"
# get opened since custom field:
cfm = ComponentManager.getInstance().getCustomFieldManager()
# get current time
currentTime = datetime.today()
# save current time
issue.setCustomFieldValue(cfm.getCustomFieldObject(opend_since_field),currentTime)
I've created a new filter to get the list of issues that are open for over 24h:
JQL:
project = XXX AND status= Open ORDER BY updated ASC, key DESC
Lastly - I've used the Jira remote API - the XML-RPC method to write a python script scheduled to run every 5 minutes. The script
reads all the issued from the filter, pulls all of them that have an 'Open' status for over 24h/36h/48h, send a reminder email, and mark them as notified, so only one reminder of each type will be sent.
The python code:
#!/usr/bin/python
# Refer to the XML-RPC Javadoc to see what calls are available:
# http://docs.atlassian.com/software/jira/docs/api/rpc-jira-plugin/latest/com/atlassian/jira/rpc/xmlrpc/XmlRpcService.html
# /home/issues_reminder.py
import xmlrpclib
import time
from time import mktime
from datetime import datetime
from datetime import timedelta
import smtplib,email
from smtplib import SMTP
from email.MIMEMultipart import MIMEMultipart
from email.MIMEBase import MIMEBase
from email.MIMEText import MIMEText
from email import Encoders
# Jira connction info
server = 'https://your.jira.com/rpc/xmlrpc'
user = 'user'
password = 'password'
filter = '10302' # Filter ID
# Email definitions
smtpserver = 'mail.server.com'
fromAddr = 'support#your.jira.com'
mail_user = 'jira_admin#your.domain.com'
mail_password = 'password'
toAddr = 'support#your.domain.com'
mysubject = "hrs Issue notification!!!"
opend_since_field = "customfield_10101"
COMMASPACE = ', '
def email_issue(issue,esc_time):
# create html email
subject = '['+issue+'] '+esc_time+mysubject
html = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" '
html +='"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml">'
html +='<body style="font-size:12px;font-family:Verdana">'
html +='<p align="center"><img src="your_logo.jpg" alt="logo" height="43" width="198"></p> '
html +='<p> The issue ['+issue+'] is open for over '+esc_time+' hours.</p>'
html +='<p> A link to view the issue: https://your.jira.com/browse/'+issue+'.</p>'
html +='<BR><p> This is an automated email sent from Jira.</p>'
html +='</body></html>'
emailMsg = email.MIMEMultipart.MIMEMultipart('alternative')
emailMsg['Subject'] = subject
emailMsg['From'] = fromAddr
emailMsg['To'] = toAddr
emailMsg.attach(MIMEText(html, 'html'))
# Send the email
emailserver = SMTP(smtpserver) # ip or domain name of smtp server
emailserver.login(mail_user, mail_password)
emailserver.sendmail(fromAddr, [toAddr], emailMsg.as_string())
emailserver.quit()
return
s = xmlrpclib.ServerProxy(server)
auth = s.jira1.login(user, password)
esc12List = []
esc24List = []
esc48List = []
issues = s.jira1.getIssuesFromFilter(auth, filter)
print "Modifying issue..."
for issue in issues:
creation = 0;
# get open since time
for customFields in issue['customFieldValues']:
if customFields['customfieldId'] == opend_since_field :
print "found field!"+ customFields['values']
creation = customFields['values']
if (creation == 0):
creation = issue['created']
print "field not found"
creationTime = datetime.fromtimestamp(mktime(time.strptime(creation, '%d/%b/%y %I:%M %p')))
currentTime = datetime.fromtimestamp(mktime(time.gmtime()))
delta = currentTime - creationTime
esc12 = timedelta(hours=12)
esc24 = timedelta(hours=24)
esc48 = timedelta(hours=48)
print "\nchecking issue "+issue['key']
if (delta < esc12):
print "less than 12 hours"
print "not updating"
continue
if (delta < esc24):
print "less than 24 hours"
for customFields in issue['customFieldValues']:
if customFields['customfieldId'] == 'customfield_10412':
if customFields['values'] == '12h':
print "not updating"
break
else:
print "updating !!!"
s.jira1.updateIssue(auth, issue['key'], {"customfield_10412": ["12h"]})
esc12List.append(issue['key'])
break
continue
if (delta < esc48):
print "less than 48 hours"
for customFields in issue['customFieldValues']:
if customFields['customfieldId'] == 'customfield_10412':
if customFields['values'] == '24h':
print "not updating"
break
else:
print "updating !!!"
s.jira1.updateIssue(auth, issue['key'], {"customfield_10412": ["24h"]})
esc24List.append(issue['key'])
break
continue
print "more than 48 hours"
for customFields in issue['customFieldValues']:
if customFields['customfieldId'] == 'customfield_10412':
if customFields['values'] == '48h':
print "not updating"
break
else:
print "updating !!!"
s.jira1.updateIssue(auth, issue['key'], {"customfield_10412": ["48h"]})
esc48List.append(issue['key'])
break
for key in esc12List:
email_issue(key,'12')
for key in esc24List:
email_issue(key,'24')
for key in esc48List:
email_issue(key,'48')
The main pros of this method is that it's highly customizable, and by saving the data to custom fields it's easy to create filters and reports to show issues that have been opened for a long time.
Escalating to the development team
Create a new transition - Escalate. This will create an issue for the development team, and link the new issue to the support issue. Add the following post function:
from com.atlassian.jira.util import ImportUtils
from com.atlassian.jira import ManagerFactory
from com.atlassian.jira.issue import MutableIssue
from com.atlassian.jira import ComponentManager
from com.atlassian.jira.issue.link import DefaultIssueLinkManager
from org.ofbiz.core.entity import GenericValue;
# get issue objects
issueManager = ComponentManager.getInstance().getIssueManager()
issueFactory = ComponentManager.getInstance().getIssueFactory()
authenticationContext = ComponentManager.getInstance().getJiraAuthenticationContext()
issueLinkManager = ComponentManager.getInstance().getIssueLinkManager()
customFieldManager = ComponentManager.getInstance().getCustomFieldManager()
userUtil = ComponentManager.getInstance().getUserUtil()
projectMgr = ComponentManager.getInstance().getProjectManager()
customer_name = customFieldManager.getCustomFieldObjectByName("Customer Name")
customer_email = customFieldManager.getCustomFieldObjectByName("Customer Email")
escalate = customFieldManager.getCustomFieldObjectByName("Escalate to Development")
if issue.getCustomFieldValue(escalate) is not None:
# define issue
issueObject = issueFactory.getIssue()
issueObject.setProject(projectMgr.getProject(10000))
issueObject.setIssueTypeId("1") # bug
# set subtask attributes
issueObject.setSummary("[Escalated from support] "+issue.getSummary())
issueObject.setAssignee(userUtil.getUserObject("nadav"))
issueObject.setReporter(issue.getAssignee())
issueObject.setDescription(issue.getDescription())
issueObject.setCustomFieldValue(customer_name, issue.getCustomFieldValue(customer_name)+" "+issue.getCustomFieldValue(customer_email))
issueObject.setComponents(issue.getComponents())
# Create subtask
subTask = issueManager.createIssue(authenticationContext.getUser(), issueObject)
# Link parent issue to subtask
issueLinkManager.createIssueLink(issueObject.getId(),issue.getId(),10003,1,authenticationContext.getUser())
# Update search indexes
ImportUtils.setIndexIssues(True);
ComponentManager.getInstance().getIndexManager().reIndex(subTask)
ImportUtils.setIndexIssues(False)
Moving to sales
reate a new transition - Move to sales. Many support calls end up as a sale call, this will move the issue to the sales team. Add the following post function:
from com.atlassian.jira.util import ImportUtils
from com.atlassian.jira.issue import MutableIssue
from com.atlassian.jira import ComponentManager
customFieldManager = ComponentManager.getInstance().getCustomFieldManager()
userUtil = ComponentManager.getInstance().getUserUtil()
issue.setStatusId("1");
issue.setAssignee(userUtil.getUserObject("John"))
issue.setSummary("[Moved from support] "+issue.getSummary())
issue.setProjectId(10201);
issue.setIssueTypeId("35");
ImportUtils.setIndexIssues(True);
ComponentManager.getInstance().getIndexManager().reIndex(issue)
ImportUtils.setIndexIssues(False)
# add to comments
from time import gmtime, strftime
time = strftime("%d-%m-%Y %H:%M:%S", gmtime())
cm = ComponentManager.getInstance().getCommentManager()
currentUser = ComponentManager.getInstance().getJiraAuthenticationContext().getUser().toString()
cm.create(issue,currentUser,'Email was moved to Sales at '+time,False)
Do you recommend Jira also for customer support or should we find
another system like for example Zendesk for that purpose?
Full disclosure: I'm the creator of DoneDone but this question is basically why our product exists.
DoneDone is a simple bug tracker and customer support/shared inbox tool rolled into one. We use it for general customer support (both via our support email address and the contact form on our website). The shared inbox tool lets you have private discussion on emails, along with allowing you to assign, prioritize, tag, and create/change statuses on them (e.g. "Open", "In Progress", etc.)
DoneDone lets you connect customer conversations (a.k.a. incoming support email) to internal tasks. So, if your company has distinct support and client-facing people while also having internal devs and you want to separate their work, you can create any number of subtasks from an incoming conversation.
If your looking for a good way to organize internal work with customer support feedback, it might be worth signing up for a free trial.

Resources