This commit is contained in:
jack ning
2025-11-14 14:13:32 +08:00
parent 378baa5899
commit f8d73dea00
33 changed files with 2360 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2025-07-01 10:00:00
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-07-01 10:00:00
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
* 联系270580156@qq.com
* Copyright (c) 2024 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email;
public enum EmailConnectionStatusEnum {
CONNECTED("已连接"),
DISCONNECTED("未连接"),
CONNECTING("连接中"),
CONNECTION_FAILED("连接失败"),
AUTHENTICATION_FAILED("认证失败"),
SYNCING("同步中"),
SYNC_FAILED("同步失败");
private final String description;
EmailConnectionStatusEnum(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}

View File

@@ -0,0 +1,164 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2024-05-11 18:14:28
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-07-26 12:43:53
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
* 联系270580156@qq.com
* Copyright (c) 2024 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email;
import java.time.ZonedDateTime;
import com.bytedesk.core.base.BaseEntity;
import com.bytedesk.core.constant.I18Consts;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import lombok.experimental.SuperBuilder;
@Entity
@Data
@SuperBuilder
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
@NoArgsConstructor
@EntityListeners({EmailEntityListener.class})
@Table(name = "bytedesk_core_email")
public class EmailEntity extends BaseEntity {
private static final long serialVersionUID = 1L;
private String name;
@Builder.Default
private String description = I18Consts.I18N_DESCRIPTION;
// 邮件服务提供商,如 QQ/GMAIL/网易 等
@Builder.Default
@Column(name = "email_provider")
private String provider = EmailProviderEnum.QQ.name();
// 用途类型:在线客服接待/工单客服 等
@Builder.Default
@Column(name = "email_type")
private String type = EmailTypeEnum.TICKET.name();
// 邮箱协议类型IMAP/POP3/SMTP/EXCHANGE
@Builder.Default
@Column(name = "email_protocol")
private String protocol = EmailProtocolEnum.IMAP.name();
// 邮箱地址
private String emailAddress;
// 邮箱密码或授权码
private String emailPassword;
// SMTP服务器地址
private String smtpHost;
// SMTP服务器端口
@Builder.Default
private Integer smtpPort = 587;
// 是否启用SSL
@Builder.Default
private Boolean smtpSslEnabled = true;
// 是否启用TLS
@Builder.Default
private Boolean smtpTlsEnabled = true;
// IMAP服务器地址
private String imapHost;
// IMAP服务器端口
@Builder.Default
private Integer imapPort = 993;
// IMAP是否启用SSL
@Builder.Default
private Boolean imapSslEnabled = true;
// POP3服务器地址
private String pop3Host;
// POP3服务器端口
@Builder.Default
private Integer pop3Port = 995;
// POP3是否启用SSL
@Builder.Default
private Boolean pop3SslEnabled = true;
// Exchange配置
// Exchange服务器地址
private String exchangeHost;
// Exchange服务器端口
@Builder.Default
private Integer exchangePort = 993;
// Exchange是否启用SSL
@Builder.Default
private Boolean exchangeSslEnabled = true;
// 发件人显示名称
private String senderName;
// 邮件显示名称(用于发送邮件时的显示名)
private String displayName;
// 邮件同步间隔(分钟)
@Builder.Default
private Integer syncInterval = 5;
// 是否自动同步邮件
@Builder.Default
private Boolean autoSyncEnabled = true;
// 是否自动回复
@Builder.Default
private Boolean autoReplyEnabled = false;
// 自动回复内容
@Column(length = 1000)
private String autoReplyContent;
// 最后同步时间
private ZonedDateTime lastSyncTime;
// 连接状态:连接成功/连接失败
@Builder.Default
private String connectionStatus = EmailConnectionStatusEnum.DISCONNECTED.name();
// 连接错误信息
@Column(length = 500)
private String connectionError;
// 是否启用,状态:启用/禁用
@Builder.Default
@Column(name = "is_enabled")
private Boolean enabled = true;
@Builder.Default
@Column(name = "is_debug")
private Boolean debug = true;
// 关联的工作组ID
private String workgroupUid;
}

View File

@@ -0,0 +1,51 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2025-02-25 09:52:34
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-03-20 17:00:07
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
*
* Copyright (c) 2025 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email;
import org.springframework.stereotype.Component;
import org.springframework.util.SerializationUtils;
import com.bytedesk.core.config.BytedeskEventPublisher;
import com.bytedesk.core.email.event.EmailCreateEvent;
import com.bytedesk.core.email.event.EmailUpdateEvent;
import com.bytedesk.core.utils.ApplicationContextHolder;
import jakarta.persistence.PostPersist;
import jakarta.persistence.PostUpdate;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component
public class EmailEntityListener {
@PostPersist
public void onPostPersist(EmailEntity email) {
log.info("onPostPersist: {}", email);
EmailEntity cloneEmail = SerializationUtils.clone(email);
//
BytedeskEventPublisher bytedeskEventPublisher = ApplicationContextHolder.getBean(BytedeskEventPublisher.class);
bytedeskEventPublisher.publishEvent(new EmailCreateEvent(cloneEmail));
}
@PostUpdate
public void onPostUpdate(EmailEntity email) {
log.info("onPostUpdate: {}", email);
EmailEntity cloneEmail = SerializationUtils.clone(email);
//
BytedeskEventPublisher bytedeskEventPublisher = ApplicationContextHolder.getBean(BytedeskEventPublisher.class);
bytedeskEventPublisher.publishEvent(new EmailUpdateEvent(cloneEmail));
}
}

View File

@@ -0,0 +1,52 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2024-08-01 06:18:10
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-07-04 18:08:08
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
* 联系270580156@qq.com
* Copyright (c) 2024 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email;
import java.time.ZonedDateTime;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import lombok.Data;
/**
* https://github.com/alibaba/easyexcel
*/
@Data
public class EmailExcel {
@ExcelProperty(index = 0, value = "标签名称")
@ColumnWidth(20)
private String name;
@ExcelProperty(index = 1, value = "类型")
@ColumnWidth(20)
private String type;
@ExcelProperty(index = 2, value = "颜色")
@ColumnWidth(20)
private String color;
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
@ExcelProperty(value = "创建时间", converter = com.bytedesk.core.converter.ZonedDateTimeConverter.class)
@ColumnWidth(25)
private ZonedDateTime createdAt;
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
@ExcelProperty(value = "修改时间", converter = com.bytedesk.core.converter.ZonedDateTimeConverter.class)
@ColumnWidth(25)
private ZonedDateTime updatedAt;
}

View File

@@ -0,0 +1,160 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2025-07-12 15:00:00
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-07-12 15:00:00
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
*
* Copyright (c) 2025 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email;
import com.alibaba.fastjson2.JSON;
import com.bytedesk.core.base.BaseExtra;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import lombok.AllArgsConstructor;
/**
* 邮件统一额外信息存储类
* 用于解析 VisitorRequest.extra 和 ThreadEntity.extra 字段中的信息,特别是当 client/channel 为 EMAIL 时
*/
@Data
@EqualsAndHashCode(callSuper = true)
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class EmailExtra extends BaseExtra {
private static final long serialVersionUID = 1L;
// ==================== 邮件基础字段 ====================
/**
* 邮件配置ID
*/
private String emailConfigUid;
/**
* 邮件地址
*/
private String emailAddress;
/**
* 邮件协议类型
*/
private String protocol;
/**
* 邮件消息ID
*/
private String messageId;
/**
* 邮件主题
*/
private String subject;
/**
* 发件人邮箱地址
*/
private String fromAddress;
/**
* 发件人姓名
*/
private String fromName;
/**
* 收件人邮箱地址
*/
private String toAddresses;
/**
* 抄送邮箱地址
*/
private String ccAddresses;
/**
* 密送邮箱地址
*/
private String bccAddresses;
/**
* 邮件内容(纯文本)
*/
private String contentText;
/**
* 邮件内容HTML
*/
private String contentHtml;
/**
* 邮件发送/接收时间
*/
private String emailDate;
/**
* 是否有附件
*/
private Boolean hasAttachments;
/**
* 附件信息
*/
private String attachments;
/**
* 邮件大小(字节)
*/
private Long emailSize;
/**
* 邮件状态
*/
private String status;
/**
* 错误信息
*/
private String errorMessage;
/**
* 回复的原邮件ID
*/
private String replyToMessageId;
/**
* 转发的原邮件ID
*/
private String forwardFromMessageId;
/**
* 邮件线程ID
*/
private String threadId;
/**
* 将对象转换为JSON字符串
* @return JSON字符串
*/
public String toJson() {
return JSON.toJSONString(this);
}
/**
* 从JSON字符串解析对象
* @param jsonString JSON字符串
* @return EmailExtra对象
*/
public static EmailExtra fromJson(String jsonString) {
return JSON.parseObject(jsonString, EmailExtra.class);
}
}

View File

@@ -0,0 +1,120 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2025-07-01 16:57:00
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-07-01 17:17:35
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
*
* Copyright (c) 2025 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import lombok.Data;
/**
* 邮件监听配置
* 用于管理不同协议的监听策略
*/
@Data
@Component
@ConfigurationProperties(prefix = "bytedesk.email.listener")
public class EmailListenerConfig {
/**
* 是否启用IDLE监听仅支持IMAP协议
*/
private boolean idleEnabled = true;
/**
* 是否启用轮询同步(支持所有协议)
*/
private boolean pollingEnabled = true;
/**
* IDLE监听检查间隔毫秒
*/
private long idleCheckInterval = 30000; // 30秒
/**
* 轮询同步最小间隔(分钟)
*/
private int minPollingInterval = 1;
/**
* 轮询同步最大间隔(分钟)
*/
private int maxPollingInterval = 60;
/**
* 连接超时时间(毫秒)
*/
private long connectionTimeout = 30000; // 30秒
/**
* 读取超时时间(毫秒)
*/
private long readTimeout = 30000; // 30秒
/**
* 重连间隔(毫秒)
*/
private long reconnectInterval = 5000; // 5秒
/**
* 最大重连次数
*/
private int maxReconnectAttempts = 3;
/**
* 是否启用调试日志
*/
private boolean debugEnabled = false;
/**
* 获取监听策略
* @param protocol 邮件协议
* @return 监听策略
*/
public ListenerStrategy getListenerStrategy(String protocol) {
if (EmailProtocolEnum.IMAP.name().equals(protocol)) {
if (idleEnabled) {
return ListenerStrategy.IDLE;
} else if (pollingEnabled) {
return ListenerStrategy.POLLING;
}
} else if (EmailProtocolEnum.POP3.name().equals(protocol) ||
EmailProtocolEnum.EXCHANGE.name().equals(protocol)) {
if (pollingEnabled) {
return ListenerStrategy.POLLING;
}
}
return ListenerStrategy.NONE;
}
/**
* 监听策略枚举
*/
public enum ListenerStrategy {
/**
* IDLE监听实时
*/
IDLE,
/**
* 轮询同步(定时)
*/
POLLING,
/**
* 无监听
*/
NONE
}
}

View File

@@ -0,0 +1,20 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2024-11-05 16:58:18
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-05-06 11:55:32
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
* 联系270580156@qq.com
* Copyright (c) 2024 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email;
import com.bytedesk.core.base.BasePermissions;
public class EmailPermissions extends BasePermissions {
}

View File

@@ -0,0 +1,21 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2025-07-01 10:40:00
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-07-01 10:40:00
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
* 联系270580156@qq.com
* Copyright (c) 2024 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email;
public enum EmailProtocolEnum {
IMAP, // IMAP邮箱
POP3, // POP3邮箱
SMTP, // SMTP邮箱
EXCHANGE // Exchange邮箱
}

View File

@@ -0,0 +1,26 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2024-07-23 17:02:46
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-11-14 10:35:00
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
*/
package com.bytedesk.core.email;
/**
* 邮件服务提供商枚举
*/
public enum EmailProviderEnum {
QQ,
TENCENT_ENTERPRISE,
SINA,
GMAIL,
NETEASE_163,
NETEASE_ENTERPRISE,
ALIYUN_ENTERPRISE,
HOTMAIL,
FEISHU_ENTERPRISE,
YAHOO,
OTHER
}

View File

@@ -0,0 +1,31 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2024-05-11 18:25:55
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-03-11 09:23:20
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
* 联系270580156@qq.com
* Copyright (c) 2024 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface EmailRepository extends JpaRepository<EmailEntity, Long>, JpaSpecificationExecutor<EmailEntity> {
Optional<EmailEntity> findByUid(String uid);
Boolean existsByUid(String uid);
List<EmailEntity> findByEnabledTrueAndDeletedFalse();
// Boolean existsByPlatform(String platform);
}

View File

@@ -0,0 +1,115 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2024-05-11 18:26:04
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-07-16 16:13:52
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
* 联系270580156@qq.com
* Copyright (c) 2024 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email;
import com.bytedesk.core.base.BaseRequest;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import lombok.experimental.SuperBuilder;
@Data
@SuperBuilder
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@AllArgsConstructor
@NoArgsConstructor
public class EmailRequest extends BaseRequest {
private static final long serialVersionUID = 1L;
private String name;
private String description;
// 邮件服务提供商QQ/GMAIL/NETEASE等
private String provider;
// 用途类型:在线客服接待/工单客服 等
// private String type;
private String protocol;
// 邮箱地址
private String emailAddress;
// 邮箱密码或授权码
private String emailPassword;
// SMTP服务器地址
private String smtpHost;
// SMTP服务器端口
private Integer smtpPort;
// 是否启用SSL
private Boolean smtpSslEnabled;
// 是否启用TLS
private Boolean smtpTlsEnabled;
// IMAP服务器地址
private String imapHost;
// IMAP服务器端口
private Integer imapPort;
// IMAP是否启用SSL
private Boolean imapSslEnabled;
// POP3服务器地址
private String pop3Host;
// POP3服务器端口
private Integer pop3Port;
// POP3是否启用SSL
private Boolean pop3SslEnabled;
// Exchange服务器地址
private String exchangeHost;
// Exchange服务器端口
private Integer exchangePort;
// Exchange是否启用SSL
private Boolean exchangeSslEnabled;
// 发件人显示名称
private String senderName;
// 邮件同步间隔(分钟)
private Integer syncInterval;
// 是否自动同步邮件
private Boolean autoSyncEnabled;
// 是否自动回复
private Boolean autoReplyEnabled;
// 自动回复内容
private String autoReplyContent;
// 是否启用,状态:启用/禁用
private Boolean enabled;
// 是否调试,状态:调试/生产
private Boolean debug;
// 关联的工作组ID
private String workgroupUid;
}

View File

@@ -0,0 +1,125 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2024-05-11 18:26:12
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-07-11 16:53:44
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
* 联系270580156@qq.com
* Copyright (c) 2024 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email;
import java.time.ZonedDateTime;
import com.bytedesk.core.base.BaseResponse;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import lombok.experimental.SuperBuilder;
@Data
@SuperBuilder
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
@NoArgsConstructor
public class EmailResponse extends BaseResponse {
private static final long serialVersionUID = 1L;
// 邮件服务提供商QQ/GMAIL/NETEASE等
private String provider;
private String name;
private String description;
// 用途类型:在线客服接待/工单客服 等
private String type;
private String protocol;
// 邮箱地址
private String emailAddress;
// 邮箱密码或授权码
private String emailPassword;
// SMTP服务器地址
private String smtpHost;
// SMTP服务器端口
private Integer smtpPort;
// 是否启用SSL
private Boolean smtpSslEnabled;
// 是否启用TLS
private Boolean smtpTlsEnabled;
// IMAP服务器地址
private String imapHost;
// IMAP服务器端口
private Integer imapPort;
// IMAP是否启用SSL
private Boolean imapSslEnabled;
// POP3服务器地址
private String pop3Host;
// POP3服务器端口
private Integer pop3Port;
// POP3是否启用SSL
private Boolean pop3SslEnabled;
// Exchange服务器地址
private String exchangeHost;
// Exchange服务器端口
private Integer exchangePort;
// Exchange是否启用SSL
private Boolean exchangeSslEnabled;
// 发件人显示名称
private String senderName;
// 邮件同步间隔(分钟)
private Integer syncInterval;
// 是否自动同步邮件
private Boolean autoSyncEnabled;
// 是否自动回复
private Boolean autoReplyEnabled;
// 自动回复内容
private String autoReplyContent;
// 最后同步时间
private ZonedDateTime lastSyncTime;
// 连接状态
private String connectionStatus;
// 连接错误信息
private String connectionError;
private Boolean enabled;
private Boolean debug;
// 关联的工作组ID
private String workgroupUid;
}

View File

@@ -0,0 +1,113 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2024-05-11 18:25:36
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-07-01 16:09:23
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
* 联系270580156@qq.com
* Copyright (c) 2024 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email;
import org.springframework.data.domain.Page;
import org.springframework.http.ResponseEntity;
// import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.bytedesk.core.annotation.ActionAnnotation;
import com.bytedesk.core.base.BaseRestController;
import com.bytedesk.core.utils.JsonResult;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
@RestController
@RequestMapping("/api/v1/email")
@AllArgsConstructor
public class EmailRestController extends BaseRestController<EmailRequest, EmailRestService> {
private final EmailRestService emailRestService;
// @PreAuthorize(RolePermissions.ROLE_ADMIN)
@ActionAnnotation(title = "Email", action = "组织查询", description = "query email by org")
@Override
public ResponseEntity<?> queryByOrg(EmailRequest request) {
Page<EmailResponse> emails = emailRestService.queryByOrg(request);
return ResponseEntity.ok(JsonResult.success(emails));
}
@ActionAnnotation(title = "Email", action = "用户查询", description = "query email by user")
@Override
public ResponseEntity<?> queryByUser(EmailRequest request) {
Page<EmailResponse> emails = emailRestService.queryByUser(request);
return ResponseEntity.ok(JsonResult.success(emails));
}
@ActionAnnotation(title = "Email", action = "查询详情", description = "query email by uid")
@Override
public ResponseEntity<?> queryByUid(EmailRequest request) {
EmailResponse email = emailRestService.queryByUid(request);
return ResponseEntity.ok(JsonResult.success(email));
}
@ActionAnnotation(title = "Email", action = "新建", description = "create email")
@Override
// @PreAuthorize("hasAuthority('EMAIL_CREATE')")
public ResponseEntity<?> create(EmailRequest request) {
EmailResponse email = emailRestService.create(request);
return ResponseEntity.ok(JsonResult.success(email));
}
@ActionAnnotation(title = "Email", action = "更新", description = "update email")
@Override
// @PreAuthorize("hasAuthority('EMAIL_UPDATE')")
public ResponseEntity<?> update(EmailRequest request) {
EmailResponse email = emailRestService.update(request);
return ResponseEntity.ok(JsonResult.success(email));
}
@ActionAnnotation(title = "Email", action = "删除", description = "delete email")
@Override
// @PreAuthorize("hasAuthority('EMAIL_DELETE')")
public ResponseEntity<?> delete(EmailRequest request) {
emailRestService.delete(request);
return ResponseEntity.ok(JsonResult.success());
}
@ActionAnnotation(title = "Email", action = "导出", description = "export email")
@Override
// @PreAuthorize("hasAuthority('TAG_EXPORT')")
@GetMapping("/export")
public Object export(EmailRequest request, HttpServletResponse response) {
return exportTemplate(
request,
response,
emailRestService,
EmailExcel.class,
"Email",
"email"
);
}
}

View File

@@ -0,0 +1,162 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2024-05-11 18:25:45
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-08-20 18:16:58
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
* 联系270580156@qq.com
* Copyright (c) 2024 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email;
import java.util.Optional;
import org.modelmapper.ModelMapper;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.bytedesk.core.base.BaseRestServiceWithExport;
import com.bytedesk.core.rbac.user.UserEntity;
import com.bytedesk.core.uid.UidUtils;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@AllArgsConstructor
public class EmailRestService extends BaseRestServiceWithExport<EmailEntity, EmailRequest, EmailResponse, EmailExcel> {
private final EmailRepository emailRepository;
private final ModelMapper modelMapper;
private final UidUtils uidUtils;
@Override
protected Specification<EmailEntity> createSpecification(EmailRequest request) {
return EmailSpecification.search(request, authService);
}
@Override
protected Page<EmailEntity> executePageQuery(Specification<EmailEntity> spec, Pageable pageable) {
return emailRepository.findAll(spec, pageable);
}
@Override
public EmailResponse queryByUid(EmailRequest request) {
Optional<EmailEntity> optional = findByUid(request.getUid());
if (optional.isPresent()) {
return convertToResponse(optional.get());
} else {
throw new RuntimeException("Email not found");
}
}
@Cacheable(value = "email", key = "#uid", unless="#result==null")
@Override
public Optional<EmailEntity> findByUid(String uid) {
return emailRepository.findByUid(uid);
}
public Boolean existsByUid(String uid) {
return emailRepository.existsByUid(uid);
}
@Override
public EmailResponse create(EmailRequest request) {
// 判断是否已经存在
if (StringUtils.hasText(request.getUid()) && existsByUid(request.getUid())) {
return convertToResponse(findByUid(request.getUid()).get());
}
//
UserEntity user = authService.getUser();
if (user != null) {
request.setUserUid(user.getUid());
}
//
EmailEntity entity = modelMapper.map(request, EmailEntity.class);
if (!StringUtils.hasText(request.getUid())) {
entity.setUid(uidUtils.getUid());
}
//
EmailEntity savedEntity = save(entity);
return convertToResponse(savedEntity);
}
@Override
public EmailResponse update(EmailRequest request) {
Optional<EmailEntity> optional = emailRepository.findByUid(request.getUid());
if (optional.isPresent()) {
EmailEntity entity = optional.get();
modelMapper.map(request, entity);
//
EmailEntity savedEntity = save(entity);
return convertToResponse(savedEntity);
}
else {
throw new RuntimeException("Email not found");
}
}
@Override
protected EmailEntity doSave(EmailEntity entity) {
return emailRepository.save(entity);
}
@Override
public EmailEntity handleOptimisticLockingFailureException(ObjectOptimisticLockingFailureException e, EmailEntity entity) {
try {
Optional<EmailEntity> latest = emailRepository.findByUid(entity.getUid());
if (latest.isPresent()) {
EmailEntity latestEntity = latest.get();
// 合并需要保留的数据
latestEntity.setName(entity.getName());
// latestEntity.setOrder(entity.getOrder());
// latestEntity.setDeleted(entity.isDeleted());
return emailRepository.save(latestEntity);
} else {
throw new RuntimeException("无法找到最新的实体数据uid: " + entity.getUid());
}
} catch (Exception ex) {
log.error("无法处理乐观锁冲突: {}", ex.getMessage(), ex);
throw new RuntimeException("无法处理乐观锁冲突: " + ex.getMessage(), ex);
}
}
@Override
public void deleteByUid(String uid) {
Optional<EmailEntity> optional = emailRepository.findByUid(uid);
if (optional.isPresent()) {
optional.get().setDeleted(true);
save(optional.get());
// emailRepository.delete(optional.get());
}
else {
throw new RuntimeException("Email not found");
}
}
@Override
public void delete(EmailRequest request) {
deleteByUid(request.getUid());
}
@Override
public EmailResponse convertToResponse(EmailEntity entity) {
return modelMapper.map(entity, EmailResponse.class);
}
@Override
public EmailExcel convertToExcel(EmailEntity entity) {
return modelMapper.map(entity, EmailExcel.class);
}
}

View File

@@ -0,0 +1,62 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2024-07-09 22:19:21
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-09-22 11:02:07
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
* 联系270580156@qq.com
* Copyright (c) 2024 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email;
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.StringUtils;
import com.bytedesk.core.base.BaseSpecification;
import com.bytedesk.core.rbac.auth.AuthService;
import jakarta.persistence.criteria.Predicate;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class EmailSpecification extends BaseSpecification<EmailEntity, EmailRequest> {
public static Specification<EmailEntity> search(EmailRequest request, AuthService authService) {
// log.info("request: {}", request);
return (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
predicates.addAll(getBasicPredicates(root, criteriaBuilder, request, authService));
// name
if (StringUtils.hasText(request.getName())) {
predicates.add(criteriaBuilder.like(root.get("name"), "%" + request.getName() + "%"));
}
// description
// if (StringUtils.hasText(request.getDescription())) {
// predicates.add(criteriaBuilder.like(root.get("description"), "%" + request.getDescription() + "%"));
// }
//
if (StringUtils.hasText(request.getUserUid())) {
predicates.add(criteriaBuilder.equal(root.get("userUid"), request.getUserUid()));
}
// searchText
if (StringUtils.hasText(request.getSearchText())) {
List<Predicate> orPredicates = new ArrayList<>();
String searchText = request.getSearchText();
orPredicates.add(criteriaBuilder.like(root.get("name"), "%" + searchText + "%"));
orPredicates.add(criteriaBuilder.like(root.get("description"), "%" + searchText + "%"));
predicates.add(criteriaBuilder.or(orPredicates.toArray(new Predicate[0])));
}
//
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
};
}
}

View File

@@ -0,0 +1,44 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2025-07-01 14:00:00
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-07-01 14:00:00
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
*
* Copyright (c) 2025 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email;
import lombok.Data;
/**
* 邮件同步概览状态
*/
@Data
public class EmailSyncOverview {
// 总的启用邮件账户数
private int totalEnabledEmails;
// 当前活跃的同步任务数
private int totalActiveTasks;
// 总的同步任务数(包括未运行的)
private int totalSyncTasks;
// 已连接的邮件账户数
private int connectedCount;
// 计算连接率
public double getConnectionRate() {
if (totalEnabledEmails == 0) {
return 0.0;
}
return (double) connectedCount / totalEnabledEmails * 100;
}
}

View File

@@ -0,0 +1,47 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2025-07-01 14:00:00
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-07-01 14:00:00
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
*
* Copyright (c) 2025 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email;
import java.time.ZonedDateTime;
import lombok.Data;
/**
* 邮件同步状态信息
*/
@Data
public class EmailSyncStatus {
// 邮件配置UID
private String emailUid;
// 是否正在运行同步任务
private boolean running;
// 是否启用
private boolean enabled;
// 是否启用自动同步
private boolean autoSyncEnabled;
// 最后同步时间
private ZonedDateTime lastSyncTime;
// 连接状态
private String connectionStatus;
// 错误信息
private String errorMessage;
}

View File

@@ -0,0 +1,24 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2024-07-23 17:02:46
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-07-01 17:53:14
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
* 联系270580156@qq.com
* Copyright (c) 2024 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email;
/**
* 邮件用途类型
* SERVICE: 在线客服接待
* TICKET: 工单客服
*/
public enum EmailTypeEnum {
SERVICE,
TICKET
}

View File

@@ -0,0 +1,36 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2025-02-25 09:59:29
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-06-10 11:35:33
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
*
* Copyright (c) 2025 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email.event;
import org.springframework.context.ApplicationEvent;
import com.bytedesk.core.email.EmailEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = false)
public class EmailCreateEvent extends ApplicationEvent {
private static final long serialVersionUID = 1L;
private EmailEntity email;
public EmailCreateEvent(EmailEntity email) {
super(email);
this.email = email;
}
}

View File

@@ -0,0 +1,35 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2025-02-25 12:31:16
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-02-25 12:31:19
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
*
* Copyright (c) 2025 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email.event;
import org.springframework.context.ApplicationEvent;
import com.bytedesk.core.email.EmailEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = false)
public class EmailDeleteEvent extends ApplicationEvent {
private static final long serialVersionUID = 1L;
private EmailEntity email;
public EmailDeleteEvent(EmailEntity email) {
super(email);
this.email = email;
}
}

View File

@@ -0,0 +1,36 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2025-02-25 09:59:29
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-02-25 10:01:00
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
*
* Copyright (c) 2025 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email.event;
import org.springframework.context.ApplicationEvent;
import com.bytedesk.core.email.EmailEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = false)
public class EmailUpdateEvent extends ApplicationEvent {
private static final long serialVersionUID = 1L;
private EmailEntity email;
public EmailUpdateEvent(EmailEntity email) {
super(email);
this.email = email;
}
}

View File

@@ -0,0 +1,4 @@
@NonNullApi
package com.bytedesk.core.email;
import org.springframework.lang.NonNullApi;

View File

@@ -0,0 +1,126 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2025-07-01 16:00:00
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-07-01 16:00:00
* @Description: 邮件编码测试工具类
*/
package com.bytedesk.core.email.util;
import lombok.extern.slf4j.Slf4j;
/**
* 邮件编码测试工具类
* 用于测试和验证邮件编码解码功能
*/
@Slf4j
public class EmailEncodingTestUtil {
/**
* 测试MIME编码解码功能
*/
public static void testMimeDecoding() {
log.info("=== 开始测试MIME编码解码功能 ===");
// 测试Base64编码的UTF-8字符串
String[] testCases = {
"=?UTF-8?B?6Zi/6YeM6YKu566x?=", // "微语AI"的Base64编码
"=?utf-8?B?5b6u5L+h5Zui6Zif?=", // "微信公众平台"的Base64编码
"=?UTF-8?B?d2VpeXU=?=", // "weiyu"的Base64编码
"=?UTF-8?Q?Test_Subject?=", // Quoted-printable编码
"=?UTF-8?B?5L2g5aW9?=", // "你好"的Base64编码
"=?UTF-8?B?6K+36L6T5YWl?=", // "欢迎"的Base64编码
"=?UTF-8?B?YnVpbGRlci5jb20=?=", // "builder.com"的Base64编码
"=?UTF-8?B?5L2g5aW95L2g5aW9?=", // "你好你好"的Base64编码
};
for (String testCase : testCases) {
String decoded = EmailEncodingUtil.decodeMimeString(testCase);
log.info("编码: {} -> 解码: {}", testCase, decoded);
}
// 测试复合地址格式
String[] addressTestCases = {
"=?UTF-8?B?6Zi/6YeM6YKu566x?= <no-reply@mailsupport.aliyun.com>",
"=?utf-8?B?5b6u5L+h5Zui6Zif?= <weixinmphelper@tencent.com>",
"=?UTF-8?B?d2VpeXU=?= <weiyu@bytedesk.com>",
"Test User <test@example.com>",
"=?UTF-8?B?5L2g5aW9?= <hello@world.com>",
};
log.info("=== 测试地址解析 ===");
for (String testCase : addressTestCases) {
String email = EmailEncodingUtil.extractEmailFromString(testCase);
String name = EmailEncodingUtil.extractNameFromString(testCase);
log.info("地址: {} -> 邮箱: {}, 姓名: {}", testCase, email, name);
}
// 测试gb18030编码新发现的问题
String[] gb18030TestCases = {
"=?gb18030?B?d2VpeXU=?=", // "weiyu"的gb18030编码
"=?GB18030?B?d2VpeXU=?=", // 大写GB18030
"=?gb18030?B?d2VpeXU=?= <weiyu@bytedesk.com>",
"=?gb18030?Q?weiyu?= <weiyu@bytedesk.com>",
};
log.info("=== 测试GB18030编码解码 ===");
for (String testCase : gb18030TestCases) {
String decoded = EmailEncodingUtil.decodeMimeString(testCase);
log.info("GB18030编码: {} -> 解码: {}", testCase, decoded);
}
log.info("=== MIME编码解码测试完成 ===");
}
/**
* 手动解码Base64编码的UTF-8字符串用于验证
*/
public static String manualDecodeBase64(String encoded) {
try {
// 移除MIME编码格式 =?UTF-8?B?...?=
String base64Part = encoded.replaceAll("=\\?UTF-8\\?B\\?(.+?)\\?=", "$1");
base64Part = base64Part.replaceAll("=\\?utf-8\\?B\\?(.+?)\\?=", "$1");
// Base64解码
byte[] decodedBytes = java.util.Base64.getDecoder().decode(base64Part);
// 转换为UTF-8字符串
return new String(decodedBytes, "UTF-8");
} catch (Exception e) {
log.error("手动解码失败: {}", e.getMessage());
return encoded;
}
}
/**
* 验证解码结果
*/
public static void verifyDecoding() {
log.info("=== 验证解码结果 ===");
String[] testCases = {
"=?UTF-8?B?6Zi/6YeM6YKu566x?=", // 应该解码为 "微语AI"
"=?utf-8?B?5b6u5L+h5Zui6Zif?=", // 应该解码为 "微信公众平台"
"=?UTF-8?B?d2VpeXU=?=", // 应该解码为 "weiyu"
};
for (String testCase : testCases) {
String autoDecoded = EmailEncodingUtil.decodeMimeString(testCase);
String manualDecoded = manualDecodeBase64(testCase);
log.info("原始: {}", testCase);
log.info("自动解码: {}", autoDecoded);
log.info("手动解码: {}", manualDecoded);
log.info("结果一致: {}", autoDecoded.equals(manualDecoded));
log.info("---");
}
}
/**
* 主方法,用于独立测试
*/
public static void main(String[] args) {
testMimeDecoding();
verifyDecoding();
}
}

View File

@@ -0,0 +1,418 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2025-07-01 16:00:00
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-07-02 08:45:48
* @Description: 邮件编码解码工具类
*/
package com.bytedesk.core.email.util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jakarta.mail.Address;
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
import jakarta.mail.Multipart;
import jakarta.mail.Part;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeUtility;
import lombok.extern.slf4j.Slf4j;
/**
* 邮件编码解码工具类
* 用于处理MIME编码的邮件头和邮件内容提取
*/
@Slf4j
public class EmailEncodingUtil {
/**
* 解码MIME编码的字符串
* 处理 =?UTF-8?B?...?= 和 =?UTF-8?Q?...?= 格式
*/
public static String decodeMimeString(String encodedString) {
if (encodedString == null || encodedString.trim().isEmpty()) {
return "";
}
try {
// 使用JavaMail的MimeUtility来解码
return MimeUtility.decodeText(encodedString);
} catch (Exception e) {
log.warn("Failed to decode MIME string: {}, error: {}", encodedString, e.getMessage());
return encodedString;
}
}
/**
* 提取并解码发件人地址
*/
public static String extractFromAddress(Message message) {
try {
if (message.getFrom() != null && message.getFrom().length > 0) {
Address fromAddress = message.getFrom()[0];
if (fromAddress instanceof InternetAddress) {
InternetAddress internetAddress = (InternetAddress) fromAddress;
return internetAddress.getAddress();
} else {
String addressStr = fromAddress.toString();
// 尝试从 "Name <email@domain.com>" 格式中提取邮箱地址
return extractEmailFromString(addressStr);
}
}
} catch (Exception e) {
log.warn("Failed to extract from address: {}", e.getMessage());
}
return "";
}
/**
* 提取并解码发件人姓名
*/
public static String extractFromName(Message message) {
try {
if (message.getFrom() != null && message.getFrom().length > 0) {
Address fromAddress = message.getFrom()[0];
if (fromAddress instanceof InternetAddress) {
InternetAddress internetAddress = (InternetAddress) fromAddress;
String personal = internetAddress.getPersonal();
if (personal != null) {
return decodeMimeString(personal);
}
}
// 如果无法获取personal尝试从完整字符串中提取
String fromStr = fromAddress.toString();
return extractNameFromString(fromStr);
}
} catch (Exception e) {
log.warn("Failed to extract from name: {}", e.getMessage());
}
return "";
}
/**
* 从字符串中提取邮箱地址
*/
public static String extractEmailFromString(String addressStr) {
if (addressStr == null) {
return "";
}
// 匹配 <email@domain.com> 格式
Pattern pattern = Pattern.compile("<([^>]+)>");
Matcher matcher = pattern.matcher(addressStr);
if (matcher.find()) {
return matcher.group(1).trim();
}
// 如果没有尖括号,直接返回原字符串
return addressStr.trim();
}
/**
* 从字符串中提取姓名
*/
public static String extractNameFromString(String addressStr) {
if (addressStr == null) {
return "";
}
// 匹配 "Name <email@domain.com>" 格式
Pattern pattern = Pattern.compile("^(.+?)\\s*<[^>]+>$");
Matcher matcher = pattern.matcher(addressStr);
if (matcher.find()) {
String name = matcher.group(1).trim();
return decodeMimeString(name);
}
// 如果没有尖括号,返回原字符串
return decodeMimeString(addressStr);
}
/**
* 提取邮件文本内容
*/
public static String extractTextContent(Message message) {
try {
if (message.isMimeType("text/plain")) {
Object content = message.getContent();
if (content instanceof String) {
return (String) content;
} else if (content instanceof InputStream) {
return readInputStream((InputStream) content);
}
} else if (message.isMimeType("multipart/*")) {
return extractTextFromMultipart(message);
}
} catch (Exception e) {
log.warn("Failed to extract text content: {}", e.getMessage());
}
return "";
}
/**
* 提取邮件HTML内容
*/
public static String extractHtmlContent(Message message) {
try {
if (message.isMimeType("text/html")) {
Object content = message.getContent();
if (content instanceof String) {
return (String) content;
} else if (content instanceof InputStream) {
return readInputStream((InputStream) content);
}
} else if (message.isMimeType("multipart/*")) {
return extractHtmlFromMultipart(message);
}
} catch (Exception e) {
log.warn("Failed to extract HTML content: {}", e.getMessage());
}
return "";
}
/**
* 从多部分邮件中提取文本内容
*/
private static String extractTextFromMultipart(Message message) {
try {
Multipart multipart = (Multipart) message.getContent();
return extractTextFromMultipart(multipart);
} catch (Exception e) {
log.warn("Failed to extract text from multipart: {}", e.getMessage());
return "";
}
}
/**
* 从多部分邮件中提取HTML内容
*/
private static String extractHtmlFromMultipart(Message message) {
try {
Multipart multipart = (Multipart) message.getContent();
return extractHtmlFromMultipart(multipart);
} catch (Exception e) {
log.warn("Failed to extract HTML from multipart: {}", e.getMessage());
return "";
}
}
/**
* 递归处理多部分邮件,提取文本内容
*/
private static String extractTextFromMultipart(Multipart multipart) throws MessagingException, IOException {
StringBuilder textContent = new StringBuilder();
for (int i = 0; i < multipart.getCount(); i++) {
Part part = multipart.getBodyPart(i);
if (part.isMimeType("text/plain")) {
Object content = part.getContent();
if (content instanceof String) {
textContent.append((String) content);
} else if (content instanceof InputStream) {
textContent.append(readInputStream((InputStream) content));
}
} else if (part.isMimeType("multipart/*")) {
textContent.append(extractTextFromMultipart((Multipart) part.getContent()));
}
}
return textContent.toString();
}
/**
* 递归处理多部分邮件提取HTML内容
*/
private static String extractHtmlFromMultipart(Multipart multipart) throws MessagingException, IOException {
StringBuilder htmlContent = new StringBuilder();
for (int i = 0; i < multipart.getCount(); i++) {
Part part = multipart.getBodyPart(i);
if (part.isMimeType("text/html")) {
Object content = part.getContent();
if (content instanceof String) {
htmlContent.append((String) content);
} else if (content instanceof InputStream) {
htmlContent.append(readInputStream((InputStream) content));
}
} else if (part.isMimeType("multipart/*")) {
htmlContent.append(extractHtmlFromMultipart((Multipart) part.getContent()));
}
}
return htmlContent.toString();
}
/**
* 从InputStream读取内容
*/
private static String readInputStream(InputStream inputStream) throws IOException {
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1) {
result.write(buffer, 0, length);
}
return result.toString(StandardCharsets.UTF_8.name());
}
/**
* 提取并解码收件人地址
*/
public static String extractToAddresses(Message message) {
try {
if (message.getAllRecipients() != null) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < message.getAllRecipients().length; i++) {
if (i > 0) sb.append(",");
Address recipient = message.getAllRecipients()[i];
if (recipient instanceof InternetAddress) {
InternetAddress internetAddress = (InternetAddress) recipient;
String address = internetAddress.getAddress();
String personal = internetAddress.getPersonal();
if (personal != null) {
// 如果有姓名,格式化为 "姓名 <邮箱>" 或直接返回邮箱
String decodedPersonal = decodeMimeString(personal);
sb.append(decodedPersonal).append(" <").append(address).append(">");
} else {
sb.append(address);
}
} else {
// 对于非InternetAddress类型尝试解码整个字符串
String addressStr = recipient.toString();
sb.append(decodeMimeString(addressStr));
}
}
return sb.toString();
}
} catch (Exception e) {
log.warn("Failed to extract to addresses: {}", e.getMessage());
}
return "";
}
/**
* 提取并解码抄送地址
*/
public static String extractCcAddresses(Message message) {
try {
if (message.getRecipients(Message.RecipientType.CC) != null) {
StringBuilder sb = new StringBuilder();
Address[] ccAddresses = message.getRecipients(Message.RecipientType.CC);
for (int i = 0; i < ccAddresses.length; i++) {
if (i > 0) sb.append(",");
Address ccAddress = ccAddresses[i];
if (ccAddress instanceof InternetAddress) {
InternetAddress internetAddress = (InternetAddress) ccAddress;
String address = internetAddress.getAddress();
String personal = internetAddress.getPersonal();
if (personal != null) {
String decodedPersonal = decodeMimeString(personal);
sb.append(decodedPersonal).append(" <").append(address).append(">");
} else {
sb.append(address);
}
} else {
String addressStr = ccAddress.toString();
sb.append(decodeMimeString(addressStr));
}
}
return sb.toString();
}
} catch (Exception e) {
log.warn("Failed to extract CC addresses: {}", e.getMessage());
}
return "";
}
/**
* 提取并解码密送地址
*/
public static String extractBccAddresses(Message message) {
try {
if (message.getRecipients(Message.RecipientType.BCC) != null) {
StringBuilder sb = new StringBuilder();
Address[] bccAddresses = message.getRecipients(Message.RecipientType.BCC);
for (int i = 0; i < bccAddresses.length; i++) {
if (i > 0) sb.append(",");
Address bccAddress = bccAddresses[i];
if (bccAddress instanceof InternetAddress) {
InternetAddress internetAddress = (InternetAddress) bccAddress;
String address = internetAddress.getAddress();
String personal = internetAddress.getPersonal();
if (personal != null) {
String decodedPersonal = decodeMimeString(personal);
sb.append(decodedPersonal).append(" <").append(address).append(">");
} else {
sb.append(address);
}
} else {
String addressStr = bccAddress.toString();
sb.append(decodeMimeString(addressStr));
}
}
return sb.toString();
}
} catch (Exception e) {
log.warn("Failed to extract BCC addresses: {}", e.getMessage());
}
return "";
}
/**
* 检查邮件是否有附件
*/
public static boolean hasAttachments(Message message) {
try {
if (message.isMimeType("multipart/*")) {
Multipart multipart = (Multipart) message.getContent();
return hasAttachmentsInMultipart(multipart);
}
return false;
} catch (Exception e) {
log.warn("Failed to check attachments: {}", e.getMessage());
return false;
}
}
/**
* 递归检查多部分邮件中是否有附件
*/
private static boolean hasAttachmentsInMultipart(Multipart multipart) throws MessagingException {
for (int i = 0; i < multipart.getCount(); i++) {
Part part = multipart.getBodyPart(i);
// 检查是否为附件
String disposition = part.getDisposition();
if (Part.ATTACHMENT.equalsIgnoreCase(disposition) ||
Part.INLINE.equalsIgnoreCase(disposition)) {
return true;
}
// 检查文件名
String fileName = part.getFileName();
if (fileName != null && !fileName.trim().isEmpty()) {
return true;
}
// 递归检查子部分
if (part.isMimeType("multipart/*")) {
try {
if (hasAttachmentsInMultipart((Multipart) part.getContent())) {
return true;
}
} catch (IOException e) {
log.warn("Failed to check attachments in multipart: {}", e.getMessage());
}
}
}
return false;
}
}

View File

@@ -0,0 +1,18 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2024-09-30 11:41:56
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2024-09-30 14:18:35
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
* 联系270580156@qq.com
* Copyright (c) 2024 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email_template;
public class EmailPushService {
}

View File

@@ -0,0 +1,48 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2024-10-28 10:15:22
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2024-10-28 10:44:26
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
* 联系270580156@qq.com
* Copyright (c) 2024 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email_template;
import com.bytedesk.core.base.BaseEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* 邮件模板
*/
@Data
@Entity
@Builder
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "bytedesk_core_email_template")
public class EmailTemplateEntity extends BaseEntity {
private static final long serialVersionUID = 1L;
private String name;
// private String subject;
private String content;
}

View File

@@ -0,0 +1,21 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2024-10-28 10:40:38
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2024-10-28 10:40:41
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
* 联系270580156@qq.com
* Copyright (c) 2024 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email_template;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface EmailTemplateRepository extends JpaRepository<EmailTemplateEntity, Long>, JpaSpecificationExecutor<EmailTemplateEntity> {
}

View File

@@ -0,0 +1,41 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2024-10-28 10:40:51
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2024-10-28 10:40:54
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
* 联系270580156@qq.com
* Copyright (c) 2024 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email_template;
import com.bytedesk.core.base.BaseRequest;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@Builder
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@AllArgsConstructor
@NoArgsConstructor
public class EmailTemplateRequest extends BaseRequest {
private static final long serialVersionUID = 1L;
private String name;
// private String subject;
// private String content;
}

View File

@@ -0,0 +1,40 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2024-10-28 10:41:00
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-07-01 10:42:52
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
* 联系270580156@qq.com
* Copyright (c) 2024 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email_template;
import com.bytedesk.core.base.BaseResponse;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@Builder
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@AllArgsConstructor
@NoArgsConstructor
public class EmailTemplateResponse extends BaseResponse {
private static final long serialVersionUID = 1L;
private String name;
// private String subject;
private String content;
}

View File

@@ -0,0 +1,27 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2024-10-28 10:40:27
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-08-20 17:41:39
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
* 联系270580156@qq.com
* Copyright (c) 2024 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email_template;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.bytedesk.core.base.BaseRestController;
@RestController
@RequestMapping("/api/v1/email-template")
public class EmailTemplateRestController extends BaseRestController<EmailTemplateRequest, EmailTemplateService> {
}

View File

@@ -0,0 +1,115 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2024-10-28 10:40:14
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-04-11 11:17:29
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
* 联系270580156@qq.com
* Copyright (c) 2024 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email_template;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Service;
import com.bytedesk.core.base.BaseRestService;
@Service
public class EmailTemplateService extends BaseRestService<EmailTemplateEntity, EmailTemplateRequest, EmailTemplateResponse> {
@Override
public Page<EmailTemplateResponse> queryByOrg(EmailTemplateRequest request) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'queryByOrg'");
}
@Override
public Page<EmailTemplateResponse> queryByUser(EmailTemplateRequest request) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'queryByUser'");
}
@Override
public Optional<EmailTemplateEntity> findByUid(String uid) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'findByUid'");
}
@Override
public EmailTemplateResponse create(EmailTemplateRequest request) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'create'");
}
@Override
public EmailTemplateResponse update(EmailTemplateRequest request) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'update'");
}
@Override
public EmailTemplateEntity save(EmailTemplateEntity entity) {
try {
return doSave(entity);
} catch (ObjectOptimisticLockingFailureException e) {
return handleOptimisticLockingFailureException(e, entity);
}
}
@Override
protected EmailTemplateEntity doSave(EmailTemplateEntity entity) {
throw new UnsupportedOperationException("实现保存逻辑");
}
@Override
public void deleteByUid(String uid) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'deleteByUid'");
}
@Override
public void delete(EmailTemplateRequest request) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'delete'");
}
@Override
public EmailTemplateEntity handleOptimisticLockingFailureException(ObjectOptimisticLockingFailureException e,
EmailTemplateEntity entity) {
throw new UnsupportedOperationException("实现乐观锁处理逻辑");
}
@Override
public EmailTemplateResponse convertToResponse(EmailTemplateEntity entity) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'convertToResponse'");
}
@Override
public EmailTemplateResponse queryByUid(EmailTemplateRequest request) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'queryByUid'");
}
@Override
protected Specification<EmailTemplateEntity> createSpecification(EmailTemplateRequest request) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'createSpecification'");
}
@Override
protected Page<EmailTemplateEntity> executePageQuery(Specification<EmailTemplateEntity> specification, Pageable pageable) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'executePageQuery'");
}
}

View File

@@ -0,0 +1,20 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2024-10-28 10:41:23
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2024-10-28 10:41:26
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
* 联系270580156@qq.com
* Copyright (c) 2024 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.core.email_template;
import com.bytedesk.core.base.BaseSpecification;
public class EmailTemplateSpecification extends BaseSpecification<EmailTemplateEntity, EmailTemplateRequest> {
}

View File

@@ -0,0 +1,4 @@
@NonNullApi
package com.bytedesk.core.email_template;
import org.springframework.lang.NonNullApi;