Selaa lähdekoodia

新增邮件接收、删除功能

Woody 1 viikko sitten
vanhempi
commit
3dd6d62de5

+ 1 - 1
framework-email/src/main/java/com/chelvc/framework/email/Body.java

@@ -8,7 +8,7 @@ import lombok.NoArgsConstructor;
 import lombok.experimental.SuperBuilder;
 
 /**
- * 邮件消息体
+ * 邮件发送消息体
  *
  * @author Woody
  * @date 2024/1/30

+ 67 - 4
framework-email/src/main/java/com/chelvc/framework/email/DefaultEmailHandler.java

@@ -3,14 +3,20 @@ package com.chelvc.framework.email;
 import java.io.File;
 import java.io.UnsupportedEncodingException;
 import java.nio.charset.StandardCharsets;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import javax.mail.Folder;
 import javax.mail.Message;
 import javax.mail.MessagingException;
 import javax.mail.Session;
+import javax.mail.Store;
 import javax.mail.Transport;
 import javax.mail.internet.InternetAddress;
 import javax.mail.internet.MimeBodyPart;
 import javax.mail.internet.MimeMessage;
+import javax.mail.search.SearchTerm;
 
+import com.chelvc.framework.common.util.ObjectUtils;
 import com.chelvc.framework.common.util.StringUtils;
 import com.chelvc.framework.email.context.EmailContextHolder;
 import lombok.NonNull;
@@ -93,21 +99,78 @@ public class DefaultEmailHandler implements EmailHandler {
         Message message = this.body2message(body, attachments);
         Transport transport = null;
         try {
-            transport = this.session.getTransport();
-            transport.connect();
+            (transport = this.session.getTransport()).connect();
             transport.sendMessage(message, message.getAllRecipients());
         } catch (Exception e) {
-            log.warn("Email send failed: {}", e.getMessage());
+            log.error("Email message send failed: {}", e.getMessage(), e);
             return false;
         } finally {
             if (transport != null) {
                 try {
                     transport.close();
                 } catch (Exception e) {
-                    log.warn("Close email transport failed: {}", e.getMessage(), e);
+                    log.warn("Email transport close failed: {}", e.getMessage(), e);
                 }
             }
         }
         return true;
     }
+
+    @Override
+    public void receive(SearchTerm search, @NonNull Consumer<Message> consumer) {
+        this.receive(search, message -> {
+            consumer.accept(message);
+            return true;
+        });
+    }
+
+    @Override
+    public void receive(SearchTerm search, @NonNull Function<Message, Boolean> function) {
+        Store store = null;
+        Folder folder = null;
+        try {
+            (store = this.session.getStore()).connect();
+            (folder = store.getFolder("INBOX")).open(Folder.READ_WRITE);
+            Message[] messages = search == null ? folder.getMessages() : folder.search(search);
+            if (ObjectUtils.notEmpty(messages)) {
+                for (Message message : messages) {
+                    // 防止邮件协议不能完全支持search命令,故需要再次匹配筛选条件
+                    if (message.match(search) && Boolean.FALSE.equals(function.apply(message))) {
+                        break;
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("Email message fetch failed: {}", e.getMessage(), e);
+        } finally {
+            if (folder != null) {
+                // 参数expunge表示是否删除标记为已删除的邮件
+                try {
+                    folder.close(true);
+                } catch (Exception e) {
+                    log.warn("Email folder close failed: {}", e.getMessage(), e);
+                }
+            }
+            if (store != null) {
+                try {
+                    store.close();
+                } catch (Exception e) {
+                    log.warn("Email store close failed: {}", e.getMessage(), e);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void delete(SearchTerm search) {
+        this.receive(search, message -> {
+            try {
+                EmailContextHolder.delete(message);
+            } catch (Exception e) {
+                log.error("Email message delete failed: {}", e.getMessage(), e);
+                return false;
+            }
+            return true;
+        });
+    }
 }

+ 83 - 0
framework-email/src/main/java/com/chelvc/framework/email/EmailHandler.java

@@ -1,7 +1,13 @@
 package com.chelvc.framework.email;
 
 import java.io.File;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import javax.mail.Message;
 import javax.mail.internet.MimeBodyPart;
+import javax.mail.search.SearchTerm;
+
+import com.chelvc.framework.common.util.ObjectUtils;
 
 /**
  * 邮件操作接口
@@ -35,4 +41,81 @@ public interface EmailHandler {
      * @return true/false
      */
     boolean send(Body body, MimeBodyPart... attachments);
+
+    /**
+     * 接收邮件
+     *
+     * @param consumer 邮件消息回调函数
+     */
+    default void receive(Consumer<Message> consumer) {
+        this.receive((SearchTerm) null, consumer);
+    }
+
+    /**
+     * 接收邮件
+     *
+     * @param function 邮件消息回调函数
+     */
+    default void receive(Function<Message, Boolean> function) {
+        this.receive((SearchTerm) null, function);
+    }
+
+    /**
+     * 接收邮件
+     *
+     * @param search   筛选条件
+     * @param consumer 邮件消息回调函数
+     */
+    default void receive(Search search, Consumer<Message> consumer) {
+        this.receive(ObjectUtils.ifNull(search, Search::term), consumer);
+    }
+
+    /**
+     * 接收邮件
+     *
+     * @param search   筛选条件
+     * @param consumer 邮件消息回调函数
+     */
+    void receive(SearchTerm search, Consumer<Message> consumer);
+
+    /**
+     * 接收邮件
+     *
+     * @param search   筛选条件
+     * @param function 邮件消息回调函数
+     */
+    default void receive(Search search, Function<Message, Boolean> function) {
+        this.receive(ObjectUtils.ifNull(search, Search::term), function);
+    }
+
+    /**
+     * 接收邮件
+     *
+     * @param search   筛选条件
+     * @param function 邮件消息回调函数
+     */
+    void receive(SearchTerm search, Function<Message, Boolean> function);
+
+    /**
+     * 删除邮件
+     */
+    default void delete() {
+        this.delete((SearchTerm) null);
+    }
+
+    /**
+     * 删除邮件
+     *
+     * @param search 筛选条件
+     */
+    default void delete(Search search) {
+        this.delete(ObjectUtils.ifNull(search, Search::term));
+    }
+
+    /**
+     * 删除邮件
+     *
+     * @param search 筛选条件
+     */
+    void delete(SearchTerm search);
 }

+ 96 - 0
framework-email/src/main/java/com/chelvc/framework/email/Search.java

@@ -0,0 +1,96 @@
+package com.chelvc.framework.email;
+
+import java.io.Serializable;
+import javax.mail.Flags;
+import javax.mail.search.AndTerm;
+import javax.mail.search.ComparisonTerm;
+import javax.mail.search.FlagTerm;
+import javax.mail.search.FromStringTerm;
+import javax.mail.search.SearchTerm;
+import javax.mail.search.SentDateTerm;
+import javax.mail.search.SubjectTerm;
+
+import com.chelvc.framework.common.model.Period;
+import com.chelvc.framework.common.util.StringUtils;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+/**
+ * 邮件筛选条件
+ *
+ * @author Woody
+ * @date 2025/5/28
+ */
+@Data
+@SuperBuilder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Search implements Serializable {
+    /**
+     * 发件人
+     */
+    private String from;
+
+    /**
+     * 是否已查看
+     */
+    private Boolean seen;
+
+    /**
+     * 时间周期
+     */
+    private Period period;
+
+    /**
+     * 邮件主题
+     */
+    private String subject;
+
+    /**
+     * 是否最近的
+     */
+    private Boolean recent;
+
+    /**
+     * 筛选条件且逻辑运算
+     *
+     * @param a 筛选条件
+     * @param b 筛选条件
+     * @return 筛选条件
+     */
+    public static SearchTerm and(SearchTerm a, SearchTerm b) {
+        return a == null ? b : b == null ? a : new AndTerm(a, b);
+    }
+
+    /**
+     * 将自定义筛选条件转换成邮件消息筛选条件
+     *
+     * @return 筛选条件实例
+     */
+    public SearchTerm term() {
+        SearchTerm term = null;
+        if (StringUtils.notEmpty(this.from)) {
+            term = new FromStringTerm(this.from);
+        }
+        if (StringUtils.notEmpty(this.subject)) {
+            term = and(term, new SubjectTerm(this.subject));
+        }
+        if (this.seen != null) {
+            term = and(term, new FlagTerm(new Flags(Flags.Flag.SEEN), this.seen));
+        }
+        if (this.recent != null) {
+            term = and(term, new FlagTerm(new Flags(Flags.Flag.RECENT), this.recent));
+        }
+        if (this.period != null) {
+            if (this.period.getBegin() != null) {
+                term = and(term, new SentDateTerm(ComparisonTerm.GE, this.period.getBegin()));
+            }
+            if (this.period.getEnd() != null) {
+                term = and(term, new SentDateTerm(ComparisonTerm.LE, this.period.getEnd()));
+            }
+        }
+        return term;
+    }
+}

+ 102 - 18
framework-email/src/main/java/com/chelvc/framework/email/context/EmailContextHolder.java

@@ -7,6 +7,7 @@ import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
+import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Objects;
@@ -17,8 +18,12 @@ import javax.activation.DataSource;
 import javax.activation.FileDataSource;
 import javax.activation.FileTypeMap;
 import javax.mail.Authenticator;
+import javax.mail.BodyPart;
+import javax.mail.Flags;
+import javax.mail.Message;
 import javax.mail.MessagingException;
 import javax.mail.Multipart;
+import javax.mail.Part;
 import javax.mail.PasswordAuthentication;
 import javax.mail.Session;
 import javax.mail.internet.MimeBodyPart;
@@ -31,6 +36,7 @@ import com.chelvc.framework.common.util.ObjectUtils;
 import com.chelvc.framework.common.util.StringUtils;
 import com.chelvc.framework.email.Body;
 import com.chelvc.framework.email.config.EmailProperties;
+import com.google.common.collect.Lists;
 import lombok.NonNull;
 
 /**
@@ -112,8 +118,8 @@ public final class EmailContextHolder {
             }
 
             // 关联文本内容里面的图片、链接
-            Matcher matcher = StringUtils.getPattern(StringUtils.IMAGE_REGEX).matcher(text);
             List<MimeBodyPart> relates = new LinkedList<>();
+            Matcher matcher = StringUtils.getPattern(StringUtils.IMAGE_REGEX).matcher(text);
             while (matcher.find()) {
                 String img = matcher.group();
                 int i = img.indexOf("\"", img.indexOf(" src")) + 1;
@@ -173,6 +179,20 @@ public final class EmailContextHolder {
         return part(file, file.getName());
     }
 
+    /**
+     * 将文件转换成邮件附件部分
+     *
+     * @param files 文件对象数组
+     * @return 邮件混合部分数组
+     */
+    public static MimeBodyPart[] part(@NonNull File... files) {
+        MimeBodyPart[] mixes = new MimeBodyPart[files.length];
+        for (int i = 0; i < files.length; i++) {
+            mixes[i] = part(files[i]);
+        }
+        return mixes;
+    }
+
     /**
      * 将文件转换成邮件附件部分
      *
@@ -181,10 +201,21 @@ public final class EmailContextHolder {
      * @return 邮件混合部分
      */
     public static MimeBodyPart part(@NonNull File file, @NonNull String comment) {
+        return part(new FileDataSource(file), comment);
+    }
+
+    /**
+     * 将文件转换成邮件附件部分
+     *
+     * @param source  数据源
+     * @param comment 文件说明
+     * @return 邮件混合部分
+     */
+    public static MimeBodyPart part(@NonNull DataSource source, @NonNull String comment) {
         MimeBodyPart body = new MimeBodyPart();
         try {
-            body.setDataHandler(new DataHandler(new FileDataSource(file)));
             body.setFileName(MimeUtility.encodeText(comment));
+            body.setDataHandler(new DataHandler(source));
         } catch (MessagingException | UnsupportedEncodingException e) {
             throw new RuntimeException(e);
         }
@@ -192,17 +223,15 @@ public final class EmailContextHolder {
     }
 
     /**
-     * 将文件转换成邮件附件部分
+     * 将文件转换成邮件附件部分
      *
-     * @param files 文件对象数组
-     * @return 邮件混合部分数组
+     * @param bytes   字节数组
+     * @param type    文件类型
+     * @param comment 文件说明
+     * @return 邮件混合部分
      */
-    public static MimeBodyPart[] part(@NonNull File... files) {
-        MimeBodyPart[] mixes = new MimeBodyPart[files.length];
-        for (int i = 0; i < files.length; i++) {
-            mixes[i] = part(files[i]);
-        }
-        return mixes;
+    public static MimeBodyPart part(@NonNull byte[] bytes, @NonNull String type, @NonNull String comment) {
+        return part(new ByteArrayDataSource(bytes, type), comment);
     }
 
     /**
@@ -214,16 +243,11 @@ public final class EmailContextHolder {
      * @return 邮件混合部分
      */
     public static MimeBodyPart part(@NonNull InputStream stream, @NonNull String type, @NonNull String comment) {
-        MimeBodyPart body = new MimeBodyPart();
         try {
-            DataSource dataSource = new ByteArrayDataSource(stream, type);
-            DataHandler dataHandler = new DataHandler(dataSource);
-            body.setDataHandler(dataHandler);
-            body.setFileName(MimeUtility.encodeText(comment));
-        } catch (IOException | MessagingException e) {
+            return part(new ByteArrayDataSource(stream, type), comment);
+        } catch (IOException e) {
             throw new RuntimeException(e);
         }
-        return body;
     }
 
     /**
@@ -264,4 +288,64 @@ public final class EmailContextHolder {
         }
         return mixed;
     }
+
+    /**
+     * 设置邮件删除标记
+     *
+     * @param message 邮件消息
+     */
+    public static void delete(@NonNull Message message) {
+        try {
+            message.setFlag(Flags.Flag.DELETED, true);
+        } catch (MessagingException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 获取附件文件名
+     *
+     * @param part 附件信息
+     * @return 文件名
+     */
+    public static String getFilename(@NonNull BodyPart part) {
+        try {
+            String filename = part.getFileName();
+            return StringUtils.isEmpty(filename) ? filename : MimeUtility.decodeText(filename);
+        } catch (MessagingException | UnsupportedEncodingException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 获取邮件附件
+     *
+     * @param message 邮件消息
+     * @return 附件列表
+     */
+    public static List<BodyPart> getAttachments(@NonNull Message message) {
+        try {
+            Object content = message.getContent();
+            if (!(content instanceof Multipart)) {
+                return Collections.emptyList();
+            }
+
+            Multipart multipart = (Multipart) content;
+            int count = multipart.getCount();
+            if (count == 0) {
+                return Collections.emptyList();
+            }
+
+            List<BodyPart> parts = Lists.newLinkedList();
+            for (int i = 0; i < count; i++) {
+                BodyPart part = multipart.getBodyPart(i);
+                if (Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition())) {
+                    parts.add(part);
+                }
+            }
+            return parts.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(parts);
+        } catch (IOException | MessagingException e) {
+            throw new RuntimeException(e);
+        }
+    }
 }