001/* 002 * acme4j - Java ACME client 003 * 004 * Copyright (C) 2021 Richard "Shred" Körber 005 * http://acme4j.shredzone.org 006 * 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 013 */ 014package org.shredzone.acme4j.smime.email; 015 016import static java.util.Objects.requireNonNull; 017import static jakarta.mail.Message.RecipientType.TO; 018import static org.shredzone.acme4j.smime.email.ResponseBodyGenerator.RESPONSE_BODY_TYPE; 019 020import edu.umd.cs.findbugs.annotations.Nullable; 021import jakarta.mail.Address; 022import jakarta.mail.Message; 023import jakarta.mail.MessagingException; 024import jakarta.mail.Session; 025import jakarta.mail.internet.InternetAddress; 026import jakarta.mail.internet.MimeMessage; 027 028/** 029 * A helper for creating an email response to the "challenge" email. 030 * <p> 031 * According to RFC-8823, the response email <em>must</em> be DKIM signed. This is 032 * <em>not</em> done by the response generator, but must be done by the outbound MTA. 033 * 034 * @see <a href="https://datatracker.ietf.org/doc/html/rfc8823">RFC 8823</a> 035 * @since 2.12 036 */ 037public class ResponseGenerator { 038 private static final int LINE_LENGTH = 72; 039 private static final String CRLF = "\r\n"; 040 041 private final EmailProcessor processor; 042 private ResponseBodyGenerator generator = this::defaultBodyGenerator; 043 private @Nullable String header; 044 private @Nullable String footer; 045 046 /** 047 * Creates a new {@link ResponseGenerator}. 048 * 049 * @param processor 050 * {@link EmailProcessor} of the challenge email. 051 */ 052 public ResponseGenerator(EmailProcessor processor) { 053 this.processor = requireNonNull(processor, "processor"); 054 } 055 056 /** 057 * Adds a custom header to the response mail body. 058 * <p> 059 * There is no need to set a header, since the response email is usually not read by 060 * humans. If a header is set, it must contain ASCII encoded plain text. 061 * 062 * @param header 063 * Header text to be used, or {@code null} if no header is to be used. 064 * @return itself 065 */ 066 public ResponseGenerator withHeader(@Nullable String header) { 067 if (header != null && !header.endsWith(CRLF)) { 068 this.header = header.concat(CRLF); 069 } else { 070 this.header = header; 071 } 072 return this; 073 } 074 075 /** 076 * Adds a custom footer to the response mail body. 077 * <p> 078 * There is no need to set a footer, since the response email is usually not read by 079 * humans. If a footer is set, it must contain ASCII encoded plain text. 080 * 081 * @param footer 082 * Footer text to be used, or {@code null} if no footer is to be used. 083 * @return itself 084 */ 085 public ResponseGenerator withFooter(@Nullable String footer) { 086 this.footer = footer; 087 return this; 088 } 089 090 /** 091 * Sets a {@link ResponseBodyGenerator} that is used for generating a response body. 092 * <p> 093 * Use this generator to individually style the email body, for example to use a 094 * multipart body. However be aware that the response mail is evaluated by a machine, 095 * and usually not read by humans, so the body should be designed as simple as 096 * possible. 097 * <p> 098 * The default body generator will just concatenate the header, the armored key 099 * authorization body, and the footer. 100 * 101 * @param generator 102 * {@link ResponseBodyGenerator} to be used, or {@code null} to use the 103 * default one. 104 * @return itself 105 */ 106 public ResponseGenerator withGenerator(@Nullable ResponseBodyGenerator generator) { 107 this.generator = generator != null ? generator : this::defaultBodyGenerator; 108 return this; 109 } 110 111 /** 112 * Generates the response email. 113 * <p> 114 * Note that according to RFC-8823, this message must have a valid DKIM or S/MIME 115 * signature. This is <em>not</em> done here, but usually performed by the outbound 116 * MTA. 117 * 118 * @param session 119 * {@code javax.mail} {@link Session} to be used for this mail. 120 * @return Generated {@link Message}. 121 */ 122 public Message generateResponse(Session session) throws MessagingException { 123 Message response = new MimeMessage(requireNonNull(session, "session")); 124 125 response.setSubject("Re: ACME: " + processor.getToken1()); 126 response.setFrom(processor.getRecipient()); 127 128 if (!processor.getReplyTo().isEmpty()) { 129 for (InternetAddress rto : processor.getReplyTo()) { 130 response.addRecipient(TO, rto); 131 } 132 } else { 133 response.addRecipients(TO, new Address[] {processor.getSender()}); 134 } 135 136 if (processor.getMessageId().isPresent()) { 137 response.setHeader("In-Reply-To", processor.getMessageId().get()); 138 } 139 140 String wrappedAuth = processor.getAuthorization() 141 .replaceAll("(.{" + LINE_LENGTH + "})", "$1" + CRLF); 142 StringBuilder responseBody = new StringBuilder(); 143 responseBody.append("-----BEGIN ACME RESPONSE-----").append(CRLF); 144 responseBody.append(wrappedAuth); 145 if (!wrappedAuth.endsWith(CRLF)) { 146 responseBody.append(CRLF); 147 } 148 responseBody.append("-----END ACME RESPONSE-----").append(CRLF); 149 150 generator.setContent(response, responseBody.toString()); 151 return response; 152 } 153 154 /** 155 * The default body generator. It just sets the response body, optionally framed by 156 * the given header and footer. 157 * 158 * @param response 159 * response {@link Message} to fill. 160 * @param responseBody 161 * Response body that must be added to the message. 162 */ 163 private void defaultBodyGenerator(Message response, String responseBody) 164 throws MessagingException { 165 StringBuilder body = new StringBuilder(); 166 if (header != null) { 167 body.append(header); 168 } 169 body.append(responseBody); 170 if (footer != null) { 171 body.append(footer); 172 } 173 response.setContent(body.toString(), RESPONSE_BODY_TYPE); 174 } 175 176}