content: package com.aizuda.snailjob.common.core.alarm.email;
  
  import cn.hutool.core.builder.Builder;
  import cn.hutool.core.io.FileUtil;
  import cn.hutool.core.io.IORuntimeException;
  import cn.hutool.core.io.IoUtil;
  import cn.hutool.core.lang.Assert;
  import cn.hutool.core.util.ArrayUtil;
  import cn.hutool.core.util.ObjectUtil;
  import cn.hutool.core.util.StrUtil;
  import com.aizuda.snailjob.common.core.util.MailUtils;
  import jakarta.activation.DataHandler;
  import jakarta.activation.DataSource;
  import jakarta.activation.FileDataSource;
  import jakarta.activation.FileTypeMap;
  import jakarta.mail.*;
  import jakarta.mail.internet.MimeBodyPart;
  import jakarta.mail.internet.MimeMessage;
  import jakarta.mail.internet.MimeMultipart;
  import jakarta.mail.internet.MimeUtility;
  import jakarta.mail.util.ByteArrayDataSource;
  
  import java.io.*;
  import java.nio.charset.Charset;
  import java.util.Date;
  
  /**
   * 邮件发送客户端
   *
   * @author looly
   * @since 3.2.0
   */
  public class Mail implements Builder<MimeMessage> {
      @Serial
      private static final long serialVersionUID = 1L;
  
      /**
       * 邮箱帐户信息以及一些客户端配置信息
       */
      private final MailAccount mailAccount;
      /**
       * 收件人列表
       */
      private String[] tos;
      /**
       * 抄送人列表（carbon copy）
       */
      private String[] ccs;
      /**
       * 密送人列表（blind carbon copy）
       */
      private String[] bccs;
      /**
       * 回复地址(reply-to)
       */
      private String[] reply;
      /**
       * 标题
       */
      private String title;
      /**
       * 内容
       */
      private String content;
      /**
       * 是否为HTML
       */
      private boolean isHtml;
      /**
       * 正文、附件和图片的混合部分
       */
      private final Multipart multipart = new MimeMultipart();
      /**
       * 是否使用全局会话，默认为false
       */
      private boolean useGlobalSession = false;
  
      /**
       * debug输出位置，可以自定义debug日志
       */
      private PrintStream debugOutput;
  
      /**
       * 创建邮件客户端
       *
       * @param mailAccount 邮件帐号
       * @return Mail
       */
      public static Mail create(MailAccount mailAccount) {
          return new Mail(mailAccount);
      }
  
  
      // --------------------------------------------------------------- Constructor start
  
      /**
       * 构造
       *
       * @param mailAccount 邮件帐户
       */
      public Mail(MailAccount mailAccount) {
          Assert.notNull(mailAccount, () -> new MailException("Email account cannot be empty"));
          this.mailAccount = mailAccount;
      }
      // --------------------------------------------------------------- Constructor end
  
      // --------------------------------------------------------------- Getters and Setters start
  
      /**
       * 设置收件人
       *
       * @param tos 收件人列表
       * @return this
       * @see #setTos(String...)
       */
      public Mail to(String... tos) {
          return setTos(tos);
      }
  
      /**
       * 设置多个收件人
       *
       * @param tos 收件人列表
       * @return this
       */
      public Mail setTos(String... tos) {
          this.tos = tos;
          return this;
      }
  
      /**
       * 设置多个抄送人（carbon copy）
       *
       * @param ccs 抄送人列表
       * @return this
       * @since 4.0.3
       */
      public Mail setCcs(String... ccs) {
          this.ccs = ccs;
          return this;
      }
  
      /**
       * 设置多个密送人（blind carbon copy）
       *
       * @param bccs 密送人列表
       * @return this
       * @since 4.0.3
       */
      public Mail setBccs(String... bccs) {
          this.bccs = bccs;
          return this;
      }
  
      /**
       * 设置多个回复地址(reply-to)
       *
       * @param reply 回复地址(reply-to)列表
       * @return this
       * @since 4.6.0
       */
      public Mail setReply(String... reply) {
          this.reply = reply;
          return this;
      }
  
      /**
       * 设置标题
       *
       * @param title 标题
       * @return this
       */
      public Mail setTitle(String title) {
          this.title = title;
          return this;
      }
  
      /**
       * 设置正文<br>
       * 正文可以是普通文本也可以是HTML（默认普通文本），可以通过调用{@link #setHtml(boolean)} 设置是否为HTML
       *
       * @param content 正文
       * @return this
       */
      public Mail setContent(String content) {
          this.content = content;
          return this;
      }
  
      /**
       * 设置是否是HTML
       *
       * @param isHtml 是否为HTML
       * @return this
       */
      public Mail setHtml(boolean isHtml) {
          this.isHtml = isHtml;
          return this;
      }
  
      /**
       * 设置正文
       *
       * @param content 正文内容
       * @param isHtml  是否为HTML
       * @return this
       */
      public Mail setContent(String content, boolean isHtml) {
          setContent(content);
          return setHtml(isHtml);
      }
  
      /**
       * 设置文件类型附件，文件可以是图片文件，此时自动设置cid（正文中引用图片），默认cid为文件名
       *
       * @param files 附件文件列表
       * @return this
       */
      public Mail setFiles(File... files) {
          if (ArrayUtil.isEmpty(files)) {
              return this;
          }
  
          final DataSource[] attachments = new DataSource[files.length];
          for (int i = 0; i < files.length; i++) {
              attachments[i] = new FileDataSource(files[i]);
          }
          return setAttachments(attachments);
      }
  
      /**
       * 增加附件或图片，附件使用{@link DataSource} 形式表示，可以使用{@link FileDataSource}包装文件表示文件附件
       *
       * @param attachments 附件列表
       * @return this
       * @since 4.0.9
       */
      public Mail setAttachments(DataSource... attachments) {
          if (ArrayUtil.isNotEmpty(attachments)) {
              final Charset charset = this.mailAccount.getCharset();
              MimeBodyPart bodyPart;
              String nameEncoded;
              try {
                  for (DataSource attachment : attachments) {
                      bodyPart = new MimeBodyPart();
                      bodyPart.setDataHandler(new DataHandler(attachment));
                      nameEncoded = attachment.getName();
                      if (this.mailAccount.isEncodefilename()) {
                          nameEncoded = InternalMailUtil.encodeText(nameEncoded, charset);
                      }
                      // 普通附件文件名
                      bodyPart.setFileName(nameEncoded);
                      if (StrUtil.startWith(attachment.getContentType(), "image/")) {
                          // 图片附件，用于正文中引用图片
                          bodyPart.setContentID(nameEncoded);
                      }
                      this.multipart.addBodyPart(bodyPart);
                  }
              } catch (MessagingException e) {
                  throw new MailException(e);
              }
          }
          return this;
      }
  
      /**
       * 增加图片，图片的键对应到邮件模板中的占位字符串，图片类型默认为"image/jpeg"
       *
       * @param cid         图片与占位符，占位符格式为cid:${cid}
       * @param imageStream 图片文件
       * @return this
       * @since 4.6.3
       */
      public Mail addImage(String cid, InputStream imageStream) {
          return addImage(cid, imageStream, null);
      }
  
      /**
       * 增加图片，图片的键对应到邮件模板中的占位字符串
       *
       * @param cid         图片与占位符，占位符格式为cid:${cid}
       * @param imageStream 图片流，不关闭
       * @param contentType 图片类型，null赋值默认的"image/jpeg"
       * @return this
       * @since 4.6.3
       */
      public Mail addImage(String cid, InputStream imageStream, String contentType) {
          ByteArrayDataSource imgSource;
          try {
              imgSource = new ByteArrayDataSource(imageStream, ObjectUtil.defaultIfNull(contentType, "image/jpeg"));
          } catch (IOException e) {
              throw new IORuntimeException(e);
          }
          imgSource.setName(cid);
          return setAttachments(imgSource);
      }
  
      /**
       * 增加图片，图片的键对应到邮件模板中的占位字符串
       *
       * @param cid       图片与占位符，占位符格式为cid:${cid}
       * @param imageFile 图片文件
       * @return this
       * @since 4.6.3
       */
      public Mail addImage(String cid, File imageFile) {
          InputStream in = null;
          try {
              in = FileUtil.getInputStream(imageFile);
              return addImage(cid, in, FileTypeMap.getDefaultFileTypeMap().getContentType(imageFile));
          } finally {
              IoUtil.close(in);
          }
      }
  
      /**
       * 设置字符集编码
       *
       * @param charset 字符集编码
       * @return this
       * @see MailAccount#setCharset(Charset)
       */
      public Mail setCharset(Charset charset) {
          this.mailAccount.setCharset(charset);
          return this;
      }
  
      /**
       * 设置是否使用全局会话，默认为true
       *
       * @param isUseGlobalSession 是否使用全局会话，默认为true
       * @return this
       * @since 4.0.2
       */
      public Mail setUseGlobalSession(boolean isUseGlobalSession) {
          this.useGlobalSession = isUseGlobalSession;
          return this;
      }
  
      /**
       * 设置debug输出位置，可以自定义debug日志
       *
       * @param debugOutput debug输出位置
       * @return this
       * @since 5.5.6
       */
      public Mail setDebugOutput(PrintStream debugOutput) {
          this.debugOutput = debugOutput;
          return this;
      }
      // --------------------------------------------------------------- Getters and Setters end
  
      @Override
      public MimeMessage build() {
          try {
              return buildMsg();
          } catch (MessagingException e) {
              throw new MailException(e);
          }
      }
  
      /**
       * 发送
       *
       * @return message-id
       * @throws MailException 邮件发送异常
       */
      public String send() throws MailException {
          try {
              return doSend();
          } catch (MessagingException e) {
              if (e instanceof SendFailedException) {
                  // 当地址无效时，显示更加详细的无效地址信息
                  final Address[] invalidAddresses = ((SendFailedException) e).getInvalidAddresses();
                  final String msg = StrUtil.format("Invalid Addresses: {}", ArrayUtil.toString(invalidAddresses));
                  throw new MailException(msg, e);
              }
              throw new MailException(e);
          }
      }
  
      // --------------------------------------------------------------- Private method start
  
      /**
       * 执行发送
       *
       * @return message-id
       * @throws MessagingException 发送异常
       */
      private String doSend() throws MessagingException {
          final MimeMessage mimeMessage = buildMsg();
          Transport.send(mimeMessage);
          return mimeMessage.getMessageID();
      }
  
      /**
       * 构建消息
       *
       * @return {@link MimeMessage}消息
       * @throws MessagingException 消息异常
       */
      private MimeMessage buildMsg() throws MessagingException {
          final Charset charset = this.mailAccount.getCharset();
          final MimeMessage msg = new MimeMessage(getSession());
          // 发件人
          final String from = this.mailAccount.getFrom();
          if (StrUtil.isEmpty(from)) {
              // 用户未提供发送方，则从Session中自动获取
              msg.setFrom();
          } else {
              msg.setFrom(InternalMailUtil.parseFirstAddress(from, charset));
          }
          // 标题
          msg.setSubject(this.title, (null == charset) ? null : charset.name());
          // 发送时间
          msg.setSentDate(new Date());
          // 内容和附件
          msg.setContent(buildContent(charset));
          // 收件人
          msg.setRecipients(MimeMessage.RecipientType.TO, InternalMailUtil.parseAddressFromStrs(this.tos, charset));
          // 抄送人
          if (ArrayUtil.isNotEmpty(this.ccs)) {
              msg.setRecipients(MimeMessage.RecipientType.CC, InternalMailUtil.parseAddressFromStrs(this.ccs, charset));
          }
          // 密送人
          if (ArrayUtil.isNotEmpty(this.bccs)) {
              msg.setRecipients(MimeMessage.RecipientType.BCC, InternalMailUtil.parseAddressFromStrs(this.bccs, charset));
          }
          // 回复地址(reply-to)
          if (ArrayUtil.isNotEmpty(this.reply)) {
              msg.setReplyTo(InternalMailUtil.parseAddressFromStrs(this.reply, charset));
          }
  
          return msg;
      }
  
      /**
       * 构建邮件信息主体
       *
       * @param charset 编码，{@code null}则使用{@link MimeUtility#getDefaultJavaCharset()}
       * @return 邮件信息主体
       * @throws MessagingException 消息异常
       */
      private Multipart buildContent(Charset charset) throws MessagingException {
          final String charsetStr = null != charset ? charset.name() : MimeUtility.getDefaultJavaCharset();
          // 正文
          final MimeBodyPart body = new MimeBodyPart();
          body.setContent(content, StrUtil.format("text/{}; charset={}", isHtml ? "html" : "plain", charsetStr));
          this.multipart.addBodyPart(body);
  
          return this.multipart;
      }
  
      /**
       * 获取默认邮件会话<br>
       * 如果为全局单例的会话，则全局只允许一个邮件帐号，否则每次发送邮件会新建一个新的会话
       *
       * @return 邮件会话 {@link Session}
       */
      private Session getSession() {
          final Session session = MailUtils.getSession(this.mailAccount, this.useGlobalSession);
  
          if (null != this.debugOutput) {
              session.setDebugOut(debugOutput);
          }
  
          return session;
      }
      // --------------------------------------------------------------- Private method end
  }
  