SpringBoot集成mail发送邮件

news2024/11/24 22:47:39

前言

发送邮件功能,借鉴 刚果商城,根据文档及项目代码实现。整理总结便有了此文,文章有不对的点,请联系博主指出,请多多点赞收藏,您的支持是我最大的动力~

发送邮件功能主要借助 mail、freemarker以及rocketmq实现。

刚果商城是个分布式项目,近看发送消息模块即可。

image-20231205211959763

标准的DDD分层架构。

RocketMQ部署

方便起见,使用docker部署环境

RocketMQ 4.5.1 安装部署

安装 NameServer

docker run -d -p 9876:9876 --name rmqnamesrv foxiswho/rocketmq:server-4.5.1

安装 Brocker

1)新建配置目录

mkdir -p /mydata/rocketmq/conf

2)新建配置文件 broker.conf

brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
# 此处为本地ip, 如果部署服务器, 需要填写服务器外网ip
brokerIP1 = xx.xx.xx.xx

3)创建容器

docker run -d \
-p 10911:10911 \
-p 10909:10909 \
--name rmqbroker \
--link rmqnamesrv:namesrv \
-v /mydata/rocketmq/conf/broker.conf:/etc/rocketmq/broker.conf \
-e "NAMESRV_ADDR=namesrv:9876" \
-e "JAVA_OPTS=-Duser.home=/opt" \
-e "JAVA_OPT_EXT=-server -Xms512m -Xmx512m" \
foxiswho/rocketmq:broker-4.5.1

安装 rocketmq 控制台

docker pull pangliang/rocketmq-console-ng
docker run -d \
--link rmqnamesrv:namesrv \
-e "JAVA_OPTS=-Drocketmq.config.namesrvAddr=namesrv:9876 -Drocketmq.config.isVIPChannel=false" \
--name rmqconsole \
-p 8088:8080 \
-t pangliang/rocketmq-console-ng

运行成功,稍等几秒启动时间,浏览器输入 ip:8088 查看。

记得放行上述所有端口,最终结果如下:

image-20231205220952290

RocketMQ安装成功~

引入主要依赖

        <!-- 发送邮件主要依赖 -->
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
        
		<!-- 模板引擎 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        
		<!-- 消息队列 实现解耦 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rocketmq</artifactId>
        </dependency>

配置文件

image-20231205212430700

主要看application.yaml 和 application-dev.yaml

application.yaml

server:
  port: 8001

spring:
  profiles:
    active: dev
  application:
    name: message-service
    stream:
      bindings:
        # 主要是如下两个通道的配置 (消费者通道)
        mailSend:
          consumer:
            concurrency: 4
            max-attempts: 1
          content-type: application/json
          destination: message-center_topic
          group: message-center_mail-send_cg
        # 生产者通道   
        messageOutput:
          content-type: application/json
          destination: message-center_topic
          group: message-center_general-send_pg
      rocketmq:
        bindings:
          mailSend:
            consumer:
              delay-level-when-next-consume: -1
              tags: common_message-center_mail-send_tag
      # ...        

application-dev.yaml

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    stream:
      rocketmq:
        binder:
          name-server: 127.0.0.1:9876 # rocketmq服务
  mail:
    default-encoding: UTF-8
    host: smtp.163.com
    password: xxx
    port: 25
    protocol: smtp
    username: xxx@163.com

重要的是mail中参数(用的是网易邮箱),username:网易邮箱账号,password:登录 SMTP server 的密码

登录 SMTP server 密码

password获取步骤如下

一、登录网页版邮箱(https://email.163.com/),进入邮箱首页。

二、点击上方设置,选择POP/SMTP/IMAP选项。

image-20231205213826857

三、在客户端协议界面,选择开启对应的协议,IMAP或者POP3分别为不同的收信协议,选择只开启需要的收信协议,比如IMAP,推荐使用IMAP协议来收发邮件,它可以和网页版完全同步。

image-20231205214131071

四、点击开启,继续开启,手机扫码发送短信后,得到的一串密码即为登录密码

image-20231205214306587

image-20231205214336717

image-20231205214600255

真正代码实现

interfaces层

image-20231205214957319

用户接口层,入参为CQRS风格,参数都在application层

发送邮件入参:

@Data
@ApiModel("邮箱发送")
public class MailSendCommand {
    
    @ApiModelProperty(value = "标题", example = "刚果商城邮箱验证码提醒")
    @NotBlank(message = "邮箱标题不能为空")
    private String title;
    
    @Email
    @ApiModelProperty(value = "发送者", example = "congomall@163.com")
    @NotBlank(message = "邮箱发送者不能为空")
    private String sender;
    
    @Email
    @ApiModelProperty(value = "接收者", example = "7798432@163.com", notes = "实际发送时更改为自己邮箱")
    @NotBlank(message = "邮箱接收者不能为空")
    private String receiver;
    
    @Email
    @ApiModelProperty("抄送者")
    private String cc;
    
    @ApiModelProperty(value = "消息参数")
    private List<String> paramList;
    
    // 与数据库对应
    @ApiModelProperty(value = "模板ID", example = "userRegisterVerification")
    @NotBlank(message = "邮箱模板ID不能为空")
    private String templateId;
}

application层

image-20231205215452481

直接调用到application层Service实现类方法,该层封装好参数直接调用基础层中消息生产者。

domain层

image-20231205215755887

领域层里面主要是一些常量、实体类,接口以及仓储接口具体实现在基础层。

infrastructure层 ☆

image-20231205220340090

image-20231205221308019

消息通道配置

source -> sink

public interface MessageSource {
    
    String OUTPUT = "messageOutput";
    
    @Output(MessageSource.OUTPUT)
    MessageChannel messageOutput();
}
public interface MessageSink {
    
    String MAIL_SEND = "mailSend";
    
    @Input(MessageSink.MAIL_SEND)
    SubscribableChannel mailSend();
}

常量与配置文件中通道名称保持一致

消息生产者

@Slf4j
@Component
@AllArgsConstructor
public class MessageSendProduce {
    
    // 属性名与配置文件中通道名保持一致
    private final MessageChannel messageOutput;
    
    /**
     * 邮箱消息发送
     */
    public void mailMessageSend(MailMessageSendEvent mailMessageSendEvent) {
        String keys = UUID.randomUUID().toString();
        Message<?> message = MessageBuilder
                .withPayload(JSON.toJSONString(mailMessageSendEvent))
                .setHeader(MessageConst.PROPERTY_KEYS, keys)
                .setHeader(MessageConst.PROPERTY_TAGS, MessageRocketMQConstants.MESSAGE_MAIL_SEND_TAG)
                .build();
        long startTime = SystemClock.now();
        boolean sendResult = false;
        try {
            // 发送消息给mq
            sendResult = messageOutput.send(message, 2000L);
        } finally {
            log.info("邮箱消息发送,发送状态: {}, Keys: {}, 执行时间: {} ms, 消息内容: {}", sendResult, keys, SystemClock.now() - startTime, JSON.toJSONString(mailMessageSendEvent));
        }
    }
}

消息消费者

@Slf4j
@Component
@RequiredArgsConstructor
public class MailMessageSendConsume {
    
    private final MessageSendFacade messageSendFacade;
    
    // 幂等性注解,还没研究
    @Idempotent(
            uniqueKeyPrefix = "mail_message_send:",
            key = "#event.messageSendId+'_'+#event.hashCode()",
            type = IdempotentTypeEnum.SPEL,
            scene = IdempotentSceneEnum.MQ,
            keyTimeout = 600L
    )
    @StreamListener(MessageSink.MAIL_SEND)
    public void mailMessageSend(@Payload MailMessageSendEvent event, @Headers Map headers) {
        long startTime = System.currentTimeMillis();
        try {
            MessageSend messageSend = BeanUtil.toBean(event, MessageSend.class);
            // 【外观模式】: 抽象消息发送、消息存储以及失败回调业务方等逻辑
            messageSendFacade.mailMessageSend(messageSend);
        } finally {
            log.info("Keys: {}, Msg id: {}, Execute time: {} ms, Message: {}", headers.get("rocketmq_KEYS"), headers.get("rocketmq_MESSAGE_ID"), System.currentTimeMillis() - startTime,
                    JSON.toJSONString(event));
        }
    }
}

外观模式:

image-20231205221631822

发送邮箱核心实现类

@Slf4j
@Component
@AllArgsConstructor
public class MailMessageProduceImpl implements ApplicationListener<ApplicationInitializingEvent>, MailMessageProduce {
    
    private final MailTemplateMapper mailTemplateMapper;
    
    private final JavaMailSender javaMailSender;
    
    private final Configuration configuration;
    
    @SneakyThrows
    @Override
    public boolean send(MessageSend messageSend) {
        try {
            // 根据模板id查询模板 模板id:userRegisterVerification
            MailTemplateDO mailTemplateDO = mailTemplateMapper.selectOne(
                    Wrappers.lambdaQuery(MailTemplateDO.class)
                            .eq(MailTemplateDO::getTemplateId, messageSend.getTemplateId()));
            MimeMessage mimeMessage = javaMailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
            helper.setFrom(messageSend.getSender());
            helper.setSubject(messageSend.getTitle());
            if (StrUtil.isNotBlank(messageSend.getCc())) {
                helper.setCc(messageSend.getCc().split(","));
            }
            if (StrUtil.isNotBlank(messageSend.getReceiver())) {
                helper.setTo(messageSend.getReceiver().split(","));
            }
            Map<String, Object> model = Maps.newHashMap();
            // 模板参数名称与下面freemarker模板中参数一一对应
            String[] templateParams = mailTemplateDO.getTemplateParam().split(",");
            if (ArrayUtil.isNotEmpty(templateParams)) {
                for (int i = 0; i < templateParams.length; i++) {
                    model.put(templateParams[i], messageSend.getParamList().get(i));
                }
            }
            // 模板id就是模板名
            String templateKey = messageSend.getTemplateId() + ".ftl";
            // 从单例对象容器获取模板
            Template template = Singleton.get(templateKey, () -> {
                try {
                    return configuration.getTemplate(templateKey);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
            helper.setText(html, true);
            // freemarker填充参数,发送邮箱
            javaMailSender.send(mimeMessage);
        } catch (Throwable ex) {
            log.error("邮件发送失败,Request: {}", JSONUtil.toJsonStr(messageSend), ex);
            return false;
        }
        return true;
    }
    
    /**
     * 初始化邮箱模板 【率先先将所有模板初始化到单例对象容器中】
     */
    @SneakyThrows
    @Override
    public void onApplicationEvent(ApplicationInitializingEvent event) {
        Resource[] resources = new PathMatchingResourcePatternResolver().getResources(ResourceUtils.CLASSPATH_URL_PREFIX + "templates/*.ftl");
        for (Resource resource : resources) {
            String templateName = resource.getFilename();
            Singleton.put(templateName, configuration.getTemplate(templateName));
        }
    }
}

image-20231205222229490

模板具体内容:

<html lang="en" xmlns:th="http://www.thymeleaf.org">
<body>
<div>
    <p style="caret-color: rgb(86, 90, 92); color: rgb(86, 90, 92); font-family: Helvetica, 微软雅黑, 宋体; text-size-adjust: auto; font-size: 20px;">
        亲爱的用户:</p>
    <p style="caret-color: rgb(86, 90, 92); color: rgb(86, 90, 92); font-family: Helvetica, 微软雅黑, 宋体; font-size: 18px; text-size-adjust: auto;">
        您好!感谢您的使用,您本次的验证码为:<span class="Apple-converted-space">&nbsp;</span></p><b
            style="font-family: Helvetica, 微软雅黑, 宋体; text-size-adjust: auto; font-size: 32px; color: rgb(45, 123, 255);">${validCode}</b>
    <p style="caret-color: rgb(86, 90, 92); color: rgb(86, 90, 92); font-family: Helvetica, 微软雅黑, 宋体; text-size-adjust: auto; font-size: 20px;">
        安全提示:</p>
    <p style="caret-color: rgb(86, 90, 92); color: rgb(86, 90, 92); font-family: Helvetica, 微软雅黑, 宋体; font-size: 18px; text-size-adjust: auto;">
        为保障您的帐户安全,请在 5 分钟内完成验证,否则验证码将自动失效。<span class="Apple-converted-space">&nbsp;</span></p>
</div>
<div>
    <includetail><!--<![endif]--></includetail>
</div>
</body>
</html>

image-20231205222432490

最终实现效果

测试结果如下:

收件为QQ邮箱:

image-20231205210059105

收件为谷歌邮箱:

image-20231205225618465

经我测试发现,配置的是网易邮箱,发送者就只能是网易邮箱,接收者可以是任意邮箱

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1287913.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MQTT框架和使用

目录 MQTT框架 1. MQTT概述 1.1 形象地理解三个角色 1.2 消息的传递 2. 在Windows上体验MQTT 2.1 安装APP 2.2 启动服务器 2.3 使用MQTTX 2.3.1 建立连接 2.3.2 订阅主题 2.3.3 发布主题 2.4 使用mosquitto 2.4.1 发布消息 2.4.2 订阅消息 3. kawaii-mqtt源码分析…

STM32下载程序的五种方法

刚开始学习 STM32 的时候&#xff0c;很多小伙伴满怀热情买好了各种设备&#xff0c;但很快就遇到了第一个拦路虎——如何将写好的代码烧进去这个黑乎乎的芯片&#xff5e; STM32 的烧录方式多样且灵活&#xff0c;可以根据实际需求选择适合的方式来将程序烧录到芯片中。本文将…

ESP32-Web-Server编程-在网页中插入图片

ESP32-Web-Server编程-在网页中插入图片 概述 图胜与言&#xff0c;在网页端显示含义清晰的图片&#xff0c;可以使得内容更容易理解。 需求及功能解析 本节演示在 ESP32 Web 服务器上插入若干图片。在插入图片时还可以对图片设置一个超链接&#xff0c;用户点击该图片时&a…

go-fastfds部署心得

我是windows系统安装 Docker Desktop部署 docker run --name go-fastdfs&#xff08;任意的一个名称&#xff09; --privilegedtrue -t -p 3666:8080 -v /data/fasttdfs_data:/data -e GO_FASTDFS_DIR/data sjqzhang/go-fastdfs:lastest docker run&#xff1a;该命令用于运…

常见测试技术都有哪些?

测试技术是用于评估系统或组件的方法&#xff0c;目的是发现它是否满足给定的要求。系统测试有助于识别缺口、错误&#xff0c;或与实际需求不同的任何类型的缺失需求。测试技术是测试团队根据给定的需求评估已开发软件所使用的最佳实践。这些技术可以确保产品或软件的整体质量…

我想修改vCenter IP地址

部署vCenter Server Appliance后&#xff0c;您可以在vCenter修改DNS设置并选择域名服务器使用。您可以编辑vCenter Server Appliance的IP地址设置。从vSphere 6.5开始正式支持vCenter修改IP地址。因此可以更改vCenter Server Appliance的IP地址和DNS设置。 注意&#xff1a;更…

AI助力智慧农业,基于YOLOv3开发构建农田场景下的庄稼作物、田间杂草智能检测识别系统

智慧农业随着数字化信息化浪潮的演变有了新的定义&#xff0c;在前面的系列博文中&#xff0c;我们从一些现实世界里面的所见所想所感进行了很多对应的实践&#xff0c;感兴趣的话可以自行移步阅读即可&#xff1a; 《自建数据集&#xff0c;基于YOLOv7开发构建农田场景下杂草…

Javaweb之前端工程打包部署的详细解析

6 打包部署 我们的前端工程开发好了&#xff0c;但是我们需要发布&#xff0c;那么如何发布呢&#xff1f;主要分为2步&#xff1a; 前端工程打包 通过nginx服务器发布前端工程 6.1 前端工程打包 接下来我们先来对前端工程进行打包 我们直接通过VS Code的NPM脚本中提供的…

Linux gtest单元测试

1 安装git sudo apt-get install git2 下载googletest git clone https://github.com/google/googletest.git3 安装googletest 注意1: 如果在 make 过程中报错,可在 CMakeLists.txt 中增加如下行,再执行下面的命令: SET(CMAKE_CXX_FLAGS “-std=c++11”) 注意2: CMakeLists…

AI助力智慧农业,基于YOLOv5全系列模型【n/s/m/l/x】开发构建不同参数量级农田场景下庄稼作物、杂草智能检测识别系统

紧接前文&#xff0c;本文是农田场景下庄稼作物、杂草检测识别的第二篇文章&#xff0c;前文是基于YOLOv3这一网络模型实现的目标检测&#xff0c;v3相对来说比较早期的网络模型了&#xff0c;本文是基于最为经典的YOLOv5来开发不同参数量级的检测端模型。 首先看下实例效果&a…

【QT】Qt常用数值输入和显示控件

目录 1.QAbstractslider 1.1主要属性 2.QSlider 2.1专有属性 2.2 常用函数 3.QScrollBar 4.QProgressBar 5.QDial 6.QLCDNumber 7.上述控件应用示例 1.QAbstractslider 1.1主要属性 QSlider、QScrollBar和Qdial3个组件都从QAbstractSlider继承而来&#xff0c;有一些共有的属性…

精准定位安全续航 无人机解决方案打造交通巡逻新模式

现代城市交通管理是城市现代化的重要组成部分&#xff0c;但传统的交通管理系统存在一系列复杂繁琐的问题&#xff0c;同时&#xff0c;交警执勤也存在较大的安全隐患。为应对这一挑战&#xff0c;复亚智能深入研究无人机技术及应用&#xff0c;推出了一套全面的无人机解决方案…

[BPE]论文实现:Neural Machine Translation of Rare Words with Subword Units

文章目录 一、完整代码二、论文解读2.1 模型架构2.2 BPE 三、过程实现四、整体总结 论文&#xff1a;Neural Machine Translation of Rare Words with Subword Units 作者&#xff1a;Rico Sennrich, Barry Haddow, Alexandra Birch 时间&#xff1a;2016 一、完整代码 这里我…

uniapp踩坑之项目:使用过滤器将时间格式化为特定格式

利用filters过滤器对数据直接进行格式化&#xff0c;注意&#xff1a;与method、onLoad、data同层级 <template><div><!-- orderInfo.time的数据为&#xff1a;2023-12-12 12:10:23 --><p>{{ orderInfo.time | formatDate }}</p> <!-- 2023-1…

D7292 双向直流电机驱动电路 ( 速度可控 ) 7V~20V 400mA,峰值电流可达1.2A 采用DIP8、SOP8的封装形式

D7292是一块带有制动和速度控制功能的双向直流电机单片电路。它可以用来驱动CDP、VCR 和 TOY等负载。该电路通过两个逻辑输入管脚的电压&#xff0c;可以控制电机正反 个方向转动以及制动。并且可以通过改变速度控制管脚的电压&#xff0c;从而方便的改变电机的速度。D7292采用…

搞笑视频无水印下载,高清无水印视频网站!

搞笑视频无水印下载这件事情一直困扰了广大网友&#xff0c;每当看见好玩好笑的搞笑视频然而下载下来的时候&#xff0c;要么画质模糊就带有水印今天分享大家几个搞笑视频无水印下载方法。 这是一个非常良心的搞笑视频无水印下载小程序水印云&#xff0c;它支持图片去水印、视…

【matlab程序】matlab画太极图|阴阳

【matlab程序】matlab画太极图|阴阳 %% 海洋与大气科学; % 时间:20231205; % clear;clc;close all; t=0:1/100000:2pi+0.00001; t1=-pi/2:1/100000:pi/2+0.00001; t2=pi/2:1/100000:3pi/2+0.00001; R=10; r=1; figure plot(Rcos(t),Rsin(t),‘color’,‘k’,‘lin…

Python下TCP编程

​ 在Python中使用socket模块的socket函数可以完成&#xff0c;语法格式如下&#xff1a; ssocket.socket(AddressFamily, Type)函数socket.socket创建一个socket&#xff0c;返回该socket的描述符。该函数带有两个参数。 Address Family&#xff1a;可以选择AF_INET&#xf…

软件测试方法之等价类测试

01 等价类划分法 1、应用场合 有数据输入的地方&#xff0c;可以使用等价类划分法。 从大量数据中挑选少量代表数据进行测试。 2、测试思想 穷举测试&#xff1a;把所有可能的数据全部测试一遍叫穷举测试。穷举测试是最全面的测试&#xff0c;但是在实际工作中不能采用&am…

Netty核心知识总结

Netty是一个高性能、异步事件驱动的NIO框架&#xff0c;它提供了对TCP、UDP和文件传输的支持&#xff0c;作为一个异步NIO框架&#xff0c;Netty的所有IO操作都是异步非阻塞的&#xff0c;通过Future-Listener机制&#xff0c;用户可以方便的主动获取或者通过通知机制获得IO操作…