PageRenderTime 90ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/jira-project/jira-components/jira-core/src/main/java/com/atlassian/jira/mail/threading/EmailHeaderBuilders.java

https://bitbucket.org/ahmed_bilal_360factors/jira7-core
Java | 198 lines | 67 code | 20 blank | 111 comment | 3 complexity | 9d48d557727e5705c11f2a1e22b6983f MD5 | raw file
Possible License(s): Apache-2.0
  1. package com.atlassian.jira.mail.threading;
  2. import com.atlassian.jira.issue.Issue;
  3. import static com.google.common.base.Preconditions.checkNotNull;
  4. import static com.google.common.base.Preconditions.checkState;
  5. /**
  6. * Responsible for holding the builders used to create the headers inserted into JIRA's notification emails.
  7. * </p>
  8. * <p>
  9. * The RFCs for email specify that headers for any messages must contain a globally unique
  10. * identifier (the Message-ID) that allows the email recipient to distinguish between new emails and
  11. * emails they have already received. When JIRA sends an email about an issue, the Message-ID header field is set to:
  12. * </p>
  13. * <p>
  14. * {@code JIRA.${issue-id}.${issue-created-date-millis}.${sequence}.${current-time-millis}@${host} }
  15. * <p>
  16. * In JIRA, Message-ID uniqueness is achieved by using the current time in milliseconds
  17. * as part of the Message-ID. However, more that one email may be sent in the same millisecond by JIRA, and so in order
  18. * to preserve global uniqueness an atomic counter is also used as part of the Message-ID, such that
  19. * no two emails sent in the same millisecond can have the same value provided by this atomic counter (the "sequence"
  20. * member of {@code MessageIdBuilder} below corresponds to this counter: the thread-safety of "sequence" is not the
  21. * responsibility of this class, and is assumed to be taken care of by the caller).
  22. * The {@code MessageIdBuilder} class below encapsulates the Message-ID generation logic in JIRA.
  23. * </p>
  24. * <p>
  25. * The "InReplyTo" header field in the email is used to keep track of what previous email a new email
  26. * is replying to. This is used by email clients to facilitate conversation threading. Typically,
  27. * the InReplyTo field will contain the Message-ID of the email that is being replied to. JIRA's use of this
  28. * field is atypical in this sense. When JIRA sends an email about an issue, the InReplyTo header field is set to:
  29. * </p>
  30. * <p>
  31. * {@code <JIRA.${issue-id}.${issue-created-date-millis}@${host}>}
  32. * <p>
  33. * This causes the recipient of the email to group this email with other emails with the same
  34. * InReplyTo header, so that hopefully emails from the same host regarding the same issue are considered part of
  35. * the same thread. How email threading works is specific to the email client, and it will not always be the case that
  36. * the InReplyTo header value will be used for this purpose, however this header value can be re-used in other
  37. * header values such as the "References" header value (i.e. Outlook),
  38. * in order to increase the likeliness that email threading
  39. * will be triggered appropriately in the client. The reason that JIRA does not simply use previous Message-IDs
  40. * for the InReplyTo header value is that multiple users may all be sent emails regarding an issue,
  41. * and each will have a different Message-ID. It is impractical for JIRA to keep track of all the previous emails
  42. * sent to different users, as this requires vast database resources.
  43. * </p>
  44. * <p>
  45. * An important difference between the {@code MessageIdBuilder} and {@code InReplyToHeaderBuilder} is that {@code MessageIdBuilder} does
  46. * not wrap the generated string with angle brackets, but {@code InReplyToHeaderBuilder} does. Both the Message-ID and
  47. * InReplyTo header fields are wrapped with angle brackets, but the {@code MessageIdBuilder} assumes that this is
  48. * done after it has transferred control back to the caller, and {@code InReplyToHeaderBuilder} encapsulates this behaviour
  49. * as part of its functionality. This is simply the result of catering for the existing callers.
  50. * </p>
  51. */
  52. public class EmailHeaderBuilders {
  53. /**
  54. * Responsible for building the Message-Id string values to be included in JIRA's issue email notifications.
  55. * <p>
  56. * This has to be globally unique and can only contain certain ASCII characters. Used to distinguish
  57. * between different emails. {@code sequence} and {@code System.currentTimeMillis()} are used to make the Message-ID
  58. * unique.
  59. * </p>
  60. * <p>
  61. * {@code JIRA.${issue-id}.${created-date-millis}.${sequence-id}.${current-time-millis}@${host} }
  62. * <p>
  63. * These message-ids are parsed by
  64. * {@link com.atlassian.jira.mail.MailThreadManager#getAssociatedIssueObject(javax.mail.Message)}
  65. * </p>
  66. */
  67. public static class MessageIdBuilder {
  68. private Issue issue;
  69. private int sequence;
  70. private String hostName;
  71. private boolean hasSequence;
  72. // JRA-37319: We drop millis by default to keep email threads if it is dropped by database/driver.
  73. private boolean dropMillis = true;
  74. public MessageIdBuilder(final Issue issue) {
  75. this.issue = issue;
  76. }
  77. public MessageIdBuilder setSequence(final int sequence) {
  78. this.sequence = sequence;
  79. hasSequence = true;
  80. return this;
  81. }
  82. public MessageIdBuilder setHostName(final String hostName) {
  83. checkNotNull(hostName);
  84. this.hostName = hostName;
  85. return this;
  86. }
  87. public MessageIdBuilder setDropMillis(final boolean dropMillis) {
  88. this.dropMillis = dropMillis;
  89. return this;
  90. }
  91. /**
  92. * {@code sequence} and {@code hostname} are required for this method to work, since they are needed
  93. * for the returned String to be valid as a Message-ID for JIRA's purposes. It is important that
  94. * the resulting String is NOT wrapped in angle brackets, even though email spec requires this, since this is
  95. * the responsibility of the caller (in contrast to {@code InReplyToHeaderBuilder}).
  96. *
  97. * @return a String representation of a Message-ID to be used as an email header value.
  98. */
  99. public String build() {
  100. checkState(hasSequence);
  101. checkNotNull(hostName);
  102. return "JIRA." + issue.getId() + '.' + getCreatedDateInMillis(issue, dropMillis) + '.' +
  103. sequence + '.' + System.currentTimeMillis() +
  104. "@" + hostName;
  105. }
  106. }
  107. /**
  108. * <p>
  109. * Responsible for building the string value for the In-Reply-To header included in JIRA's email notifications.
  110. * Format of String is:
  111. * </p>
  112. * <p>
  113. * {@code <JIRA.${issue-id}.${created-date-millis}@${hostY}> }
  114. * </p>
  115. * <p>
  116. * {@code created-date-millis} can be the String "null" if at some point the issue corresponding to
  117. * {@code issue-id} had its created-date set to null.
  118. * </p>
  119. */
  120. public static class InReplyToHeaderBuilder {
  121. private Issue issue;
  122. private String hostName;
  123. // JRA-37319: We drop millis by default to keep email threads if it is dropped by database/driver.
  124. private boolean dropMillis = true;
  125. public InReplyToHeaderBuilder(final Issue issue) {
  126. this.issue = issue;
  127. }
  128. public InReplyToHeaderBuilder setHostName(final String hostName) {
  129. this.hostName = hostName;
  130. return this;
  131. }
  132. public InReplyToHeaderBuilder setDropMillis(final boolean dropMillis) {
  133. this.dropMillis = dropMillis;
  134. return this;
  135. }
  136. /**
  137. * It is important that this method wraps the resulting String with angle brackets, since this is
  138. * NOT the responsibility of the caller, in contrast to the build method in the {@code MessageIdBuilder} class.
  139. *
  140. * @return a String representation of the In-Reply-To email header value.
  141. */
  142. public String build() {
  143. checkNotNull(hostName);
  144. return "<JIRA." + issue.getId() + "." + getCreatedDateInMillis(issue, dropMillis) + "@" + hostName + ">";
  145. }
  146. }
  147. /**
  148. * <p>
  149. * Returns the createdDateInMillis of {@code issue} as a string. This method can return the string "null"
  150. * if the created-date for {@code issue} is null. This can occur if the jiraissue table in the DB becomes corrupted
  151. * and the created-date of the issue is somehow nullified. If this happens, the work-around is to return "null"
  152. * here and use this where ever the created-date string might have been used. One implication of this is that
  153. * all emails with the InReplyTo header field generated by {@code InReplyToHeaderBuilder} equal to:
  154. * </p>
  155. * <p>
  156. * {@code <JIRA.${issueX-id}.null@${hostY}> }
  157. * <p>
  158. * will be considered by JIRA to be referencing the same issue, even though these emails may be referencing
  159. * issues on different servers that share the same issue-id. Given that database corruption is a possibility,
  160. * this is the best work-around currently available.
  161. * </p>
  162. *
  163. * @param issue the Issue to get the createdDateInMillis for.
  164. * @return createdDateInMillis for {@code issue} as a String
  165. */
  166. private static String getCreatedDateInMillis(final Issue issue, boolean dropMillis) {
  167. if (issue.getCreated() == null) {
  168. return "null";
  169. }
  170. long issueCreated = issue.getCreated().getTime();
  171. if (dropMillis) {
  172. // JRA-37319 Round off the millis because on some DBs this gets dropped between create issue and update issue
  173. // We want to leave the 000 on the end so we can still recognise old Message-ID's that were previously created.
  174. issueCreated = issueCreated - (issueCreated % 1000);
  175. }
  176. return Long.toString(issueCreated);
  177. }
  178. }