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 = message.headers.get("Subject");
 91         var author = EmailAddress.parse(message.headers.get("From"));
 92         var sender = author;
 93         if (message.headers.containsKey("Sender")) {
 94             sender = EmailAddress.parse(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(EmailAddress::parse)
100                                .collect(Collectors.toList());
101         } else {
102             recipients = List.of();
103         }
104 
105         // Remove all known headers
106         var filteredHeaders = message.headers.entrySet().stream()
107                                              .filter(entry -> !entry.getKey().equalsIgnoreCase("Message-Id"))
108                                              .filter(entry -> !entry.getKey().equalsIgnoreCase("Date"))
109                                              .filter(entry -> !entry.getKey().equalsIgnoreCase("Subject"))
110                                              .filter(entry -> !entry.getKey().equalsIgnoreCase("From"))
111                                              .filter(entry -> !entry.getKey().equalsIgnoreCase("Sender"))
112                                              .filter(entry -> !entry.getKey().equalsIgnoreCase("To"))
113                                              .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
114 
115         return new Email(id, date, recipients, author, sender, subject, message.body, filteredHeaders);
116     }
117 
118     public static EmailBuilder create(EmailAddress author, String subject, String body) {
119         return new EmailBuilder(author, subject, body);
120     }
121 
122     public static EmailBuilder create(String subject, String body) {
123         return new EmailBuilder(subject, body);
124     }
125 
126     public static EmailBuilder from(Email email) {
127         return new EmailBuilder(email.author, email.subject, email.body)
128                 .sender(email.sender)
129                 .recipients(email.recipients)
130                 .id(email.id)
131                 .date(email.date)
132                 .headers(email.headers);
133     }
134 
135     public static EmailBuilder reply(Email parent, String subject, String body) {
136         var references = parent.id().toString();
137         if (parent.hasHeader("References")) {
138             references = parent.headerValue("References") + " " + references;
139         }
140 
141         return new EmailBuilder(subject, body)
142                 .header("In-Reply-To", parent.id().toString())
143                 .header("References", references);
144     }
145 
146     @Override
147     public boolean equals(Object o) {
148         if (this == o) {
149             return true;
150         }
151         if (o == null || getClass() != o.getClass()) {
152             return false;
153         }
154         Email email = (Email) o;
155         return id.equals(email.id) &&
156                 date.toEpochSecond() == email.date.toEpochSecond() &&
157                 recipients.equals(email.recipients) &&
158                 author.equals(email.author) &&
159                 sender.equals(email.sender) &&
160                 subject.equals(email.subject) &&
161                 body.equals(email.body) &&
162                 headers.equals(email.headers);
163     }
164 
165     @Override
166     public int hashCode() {
167         return Objects.hash(id, date.toEpochSecond(), recipients, author, sender, subject, body, headers);
168     }
169 
170     public EmailAddress id() {
171         return id;
172     }
173 
174     public List<EmailAddress> recipients() {
175         return new ArrayList<>(recipients);
176     }
177 
178     public EmailAddress author() {
179         return author;
180     }
181 
182     public EmailAddress sender() {
183         return sender;
184     }
185 
186     public ZonedDateTime date() {
187         return date;
188     }
189 
190     public String subject() {
191         return subject;
192     }
193 
194     public String body() {
195         return body;
196     }
197 
198     public Set<String> headers() {
199         return new HashSet<>(headers.keySet());
200     }
201 
202     public boolean hasHeader(String header) {
203         return headers.containsKey(header);
204     }
205 
206     public String headerValue(String header) {
207         return headers.get(header);
208     }
209 
210     @Override
211     public String toString() {
212         return "Email{" +
213                 "id='" + id + '\'' +
214                 ", date=" + date +
215                 ", recipients=" + recipients +
216                 ", author=" + author +
217                 ", sender=" + sender +
218                 ", subject='" + subject + '\'' +
219                 ", body='" + body + '\'' +
220                 ", headers=" + headers +
221                 '}';
222     }
223 }