1 /* 2 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 package org.openjdk.skara.email; 24 25 import java.time.ZonedDateTime; 26 import java.time.format.*; 27 import java.time.temporal.ChronoUnit; 28 import java.util.*; 29 import java.util.regex.Pattern; 30 import java.util.stream.Collectors; 31 32 public class Email { 33 private final EmailAddress id; 34 private final ZonedDateTime date; 35 private final List<EmailAddress> recipients; 36 private final EmailAddress author; 37 private final EmailAddress sender; 38 private final String subject; 39 private final String body; 40 private final Map<String, String> headers; 41 42 private final static Pattern mboxMessageHeaderBodyPattern = Pattern.compile( 43 "\\R{2}", Pattern.MULTILINE); 44 private final static Pattern mboxMessageHeaderPattern = Pattern.compile( 45 "^([-\\w]+): ((?:.(?!\\R\\w))*.)", Pattern.MULTILINE | Pattern.DOTALL); 46 47 Email(EmailAddress id, ZonedDateTime date, List<EmailAddress> recipients, EmailAddress author, EmailAddress sender, String subject, String body, Map<String, String> headers) { 48 this.id = id; 49 this.date = date.truncatedTo(ChronoUnit.SECONDS); 50 this.recipients = new ArrayList<>(recipients); 51 this.sender = sender; 52 this.subject = subject; 53 this.body = body; 54 this.author = author; 55 this.headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 56 this.headers.putAll(headers); 57 } 58 59 private static class MboxMessage { 60 Map<String, String> headers; 61 String body; 62 } 63 64 private static MboxMessage parseMboxMessage(String message) { 65 var ret = new MboxMessage(); 66 67 var parts = mboxMessageHeaderBodyPattern.split(message, 2); 68 var headers = mboxMessageHeaderPattern.matcher(parts[0]).results() 69 .collect(Collectors.toMap(match -> match.group(1), 70 match -> match.group(2) 71 .replaceAll("\\R", ""))); 72 ret.headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 73 ret.headers.putAll(headers); 74 ret.body = parts[1].stripTrailing(); 75 return ret; 76 } 77 78 private static final Pattern redundantTimeZonePattern = Pattern.compile("^(.*[-+\\d{4}]) \\(\\w+\\)$"); 79 80 public static Email parse(String raw) { 81 var message = parseMboxMessage(raw); 82 83 var id = EmailAddress.parse(message.headers.get("Message-Id")); 84 var unparsedDate = message.headers.get("Date"); 85 var redundantTimeZonePatternMatcher = redundantTimeZonePattern.matcher(unparsedDate); 86 if (redundantTimeZonePatternMatcher.matches()) { 87 unparsedDate = redundantTimeZonePatternMatcher.group(1); 88 } 89 var date = ZonedDateTime.parse(unparsedDate, DateTimeFormatter.RFC_1123_DATE_TIME); 90 var subject = MimeText.decode(message.headers.get("Subject")); 91 var author = EmailAddress.parse(MimeText.decode(message.headers.get("From"))); 92 var sender = author; 93 if (message.headers.containsKey("Sender")) { 94 sender = EmailAddress.parse(MimeText.decode(message.headers.get("Sender"))); 95 } 96 List<EmailAddress> recipients; 97 if (message.headers.containsKey("To")) { 98 recipients = Arrays.stream(message.headers.get("To").split(",")) 99 .map(MimeText::decode) 100 .map(EmailAddress::parse) 101 .collect(Collectors.toList()); 102 } else { 103 recipients = List.of(); 104 } 105 106 // Remove all known headers 107 var filteredHeaders = message.headers.entrySet().stream() 108 .filter(entry -> !entry.getKey().equalsIgnoreCase("Message-Id")) 109 .filter(entry -> !entry.getKey().equalsIgnoreCase("Date")) 110 .filter(entry -> !entry.getKey().equalsIgnoreCase("Subject")) 111 .filter(entry -> !entry.getKey().equalsIgnoreCase("From")) 112 .filter(entry -> !entry.getKey().equalsIgnoreCase("Sender")) 113 .filter(entry -> !entry.getKey().equalsIgnoreCase("To")) 114 .collect(Collectors.toMap(Map.Entry::getKey, 115 entry -> MimeText.decode(entry.getValue()))); 116 117 return new Email(id, date, recipients, author, sender, subject, MimeText.decode(message.body), filteredHeaders); 118 } 119 120 public static EmailBuilder create(EmailAddress author, String subject, String body) { 121 return new EmailBuilder(author, subject, body); 122 } 123 124 public static EmailBuilder create(String subject, String body) { 125 return new EmailBuilder(subject, body); 126 } 127 128 public static EmailBuilder from(Email email) { 129 return new EmailBuilder(email.author, email.subject, email.body) 130 .sender(email.sender) 131 .recipients(email.recipients) 132 .id(email.id) 133 .date(email.date) 134 .headers(email.headers); 135 } 136 137 public static EmailBuilder reply(Email parent, String subject, String body) { 138 var references = parent.id().toString(); 139 if (parent.hasHeader("References")) { 140 references = parent.headerValue("References") + " " + references; 141 } 142 143 return new EmailBuilder(subject, body) 144 .header("In-Reply-To", parent.id().toString()) 145 .header("References", references); 146 } 147 148 @Override 149 public boolean equals(Object o) { 150 if (this == o) { 151 return true; 152 } 153 if (o == null || getClass() != o.getClass()) { 154 return false; 155 } 156 Email email = (Email) o; 157 return id.equals(email.id) && 158 date.toEpochSecond() == email.date.toEpochSecond() && 159 recipients.equals(email.recipients) && 160 author.equals(email.author) && 161 sender.equals(email.sender) && 162 subject.equals(email.subject) && 163 body.equals(email.body) && 164 headers.equals(email.headers); 165 } 166 167 @Override 168 public int hashCode() { 169 return Objects.hash(id, date.toEpochSecond(), recipients, author, sender, subject, body, headers); 170 } 171 172 public EmailAddress id() { 173 return id; 174 } 175 176 public List<EmailAddress> recipients() { 177 return new ArrayList<>(recipients); 178 } 179 180 public EmailAddress author() { 181 return author; 182 } 183 184 public EmailAddress sender() { 185 return sender; 186 } 187 188 public ZonedDateTime date() { 189 return date; 190 } 191 192 public String subject() { 193 return subject; 194 } 195 196 public String body() { 197 return body; 198 } 199 200 public Set<String> headers() { 201 return new HashSet<>(headers.keySet()); 202 } 203 204 public boolean hasHeader(String header) { 205 return headers.containsKey(header); 206 } 207 208 public String headerValue(String header) { 209 return headers.get(header); 210 } 211 212 @Override 213 public String toString() { 214 return "Email{" + 215 "id='" + id + '\'' + 216 ", date=" + date + 217 ", recipients=" + recipients + 218 ", author=" + author + 219 ", sender=" + sender + 220 ", subject='" + subject + '\'' + 221 ", body='" + body + '\'' + 222 ", headers=" + headers + 223 '}'; 224 } 225 }