邮件发送功能实现与代码规范化

一、提示样式

优化前:

我们可能需要根据不同的情境展示不同的图标。这可以通过判断variant参数来实现,如下所示

ts 复制代码
// src/components/ui/toaster.tsx
{props.variant === 'default' ? (
  <CheckCircle className="text-primary" />
) : (
  <CircleOff />
)}

优化后:

对于安装的shadcn组件,都是可以自行修改的,package中虽然有记录,但是重新安装包时也不会覆盖,当你想再次添加该组件时会询问Component toast already exists. Would you like to overwrite?

二、提示文案

以下是整理的注册相关的提示内容,导出了一个getMessages方法,会根据code参数返回对应的提示文案,并在需要的时候替换文案中的字段。validatorMessages内是格式校验文案直接引入即可。

ts 复制代码
// src/lib/tips.ts
const messages = {
  '9997': '密码必须包含数字、大写字母、小写字母和符号,长度为8 ~ 20个字符',
  '9998': '邮箱激活码必填',
  '9999': '服务端走神了,请联系管理员',
  '10000': '未知错误',
  '10001': '邮箱格式不正确',
  '10002': '两次输入密码不一致',
  '10003': '账号注册',
  '10004': '用户${field}注册成功',
  '10005': '激活邮箱',
  '10006': '激活邮件已发送,请前往邮箱查看',
  '10007': '请输入正确的邮箱,然后激活',
  '10008': '输入邮箱',
  '10009': '输入登录密码',
  '10010': '再次输入登录密码',
  '10011': '输入邮箱激活码',
  '10012': '客户端缺少参数',
  '10013': '邮箱已经注册,请直接登录',
  '10014': '激活码不存在,点击激活获取',
  '10015': '激活码已过期,请重新发送获取',
  '10016': '激活码不正确,请重新输入',
  '10017': '发送邮件失败,请检查邮箱是否正确',
}

export const validatorMessages = {
  email: messages['10001'],
  password: messages['9997'],
  emailCode: messages['9998'],
}

export type TipsCode = keyof typeof messages

export const getMessages = (code: TipsCode, field?: string) => {
  if (field && messages[code]) return messages[code].replace('${field}', field)
  return messages[code] ? messages[code] : messages['10000']
}

三、异常处理

在处理异常时,如果是主动抛出的错误,我们可以直接抛出错误信息;如果不是主动抛出的错误,为了防止暴露敏感信息,我们可以抛出一个固定的服务端错误提示。以下是一个示例

ts 复制代码
// src/lib/utils.ts
import { TRPCError } from '@trpc/server'
import { getMessages } from './tips'
import { TRPC_ERROR_CODE_KEY } from '@trpc/server/rpc'
export class ManualTRPCError extends TRPCError {
  constructor(code: TRPC_ERROR_CODE_KEY, message?: string) {
    super({
      code: code,
      message: message,
    })
    this.name = 'ManualTRPCError'
  }
}

export function handleErrorforInitiative(error: any) {
  if (error instanceof ManualTRPCError) {
    throw error
  } else {
    throw new ManualTRPCError('INTERNAL_SERVER_ERROR', getMessages('9999'))
  }
}

这段代码首先定义了一个ManualTRPCError类,该类继承自TRPCError类,然后定义了一个handleErrorforInitiative函数,该函数接受一个错误对象作为参数,如果错误对象是ManualTRPCError的实例,则直接抛出该错误;否则,抛出一个新的ManualTRPCError错误,并设置错误信息为服务端错误提示。

在trpc的路由中使用trycatch捕获错误示例:

ts 复制代码
emailRegister: publicProcedure
    .input(
      z.object({
        email: z.string(),
        password: z.string(),
        emailCode: z.string(),
      })
    )
    .mutation(async ({ input }) => {
      try {
        const { email, password, emailCode } = input
        // 验证参数
        if (!email || !password || !emailCode) {
          throw new ManualTRPCError('BAD_REQUEST', getMessages('10012'))
        }
        // ...
        return { id: newUser.id, email }
      } catch (error) {
        handleErrorforInitiative(error)
      }
    }),

同时完成emailRegister和emailActive的错误处理

四、发送邮件

我们可以使用nodemailer库来发送邮件,安装所需的包

bash 复制代码
npm i nodemailer
npm i --save-dev @types/nodemailer

以QQ邮箱为例,根据以下获取授权码,然后进行环境变量配置

.env.local中配置以下环境变量,USERPASS是变化的,其他两个不需要变,PASS是申请的授权码,同时配置到.env.example

ini 复制代码
# email-send
EMAIL_HOST="smtp.qq.com"
EMAIL_PORT="587"
EMAIL_USER="...@qq.com"
EMAIL_PASS="..."
DOMAIN="example.com"

创建一个发送邮件的工具函数,后续默认完成了message的文案

ts 复制代码
// src/lib/sendEmail.ts
import nodemailer from 'nodemailer'
import { ManualTRPCError } from './utils'
import { getMessages } from './tips'

const transporter = nodemailer.createTransport({
  host: process.env.EMAIL_HOST,
  port: Number(process.env.EMAIL_PORT),
  secure: false,
  auth: {
    user: process.env.EMAIL_USER, //我的邮箱
    pass: process.env.EMAIL_PASS, //授权码
  },
})

interface IEmailOptions {
  to: string
  subject: string
  text?: string
  html?: string
}

export const sendEmail = async ({ to, subject, text, html }: IEmailOptions) => {
  try {
    const info = await transporter.sendMail({
      from: process.env.EMAIL_USER, // sender address
      to, // list of receivers
      subject, // Subject line
      text, // plain text body
      html, // html body
    })
    return info
  } catch (error) {
    // 失败时候的处理
    throw new ManualTRPCError('BAD_REQUEST', getMessages('10017'))
  }
}

创建邮件模板

ts 复制代码
// src/lib/utils.ts
export function getEmailTemplate(hashedEmail: string, sendEmail: string) {
  const sendInfo = {
    hashedemail: hashedEmail,
    url: process.env.NEXTAUTH_URL,
    domain: process.env.DOMAIN,
    sendEmail: sendEmail,
    a: '您好',
    b: '欢迎您注册为我们的用户,以下是验证秘钥:',
    c: '为了您的安全,秘钥将在24小时后过期。',
    d: '如果不是您本人注册为我们的用户,请安全的忽略该邮件。',
    e: '这个信息是从',
    f: '发出到',
  }
  return `
  <div class="mailMainArea" style="font-size: 14px; font-family: Verdana, 宋体, Helvetica, sans-serif; line-height: 1.66; padding: 8px 10px; margin: 0px; width: 700px;"><table border="0" cellpadding="0" cellspacing="0" class="" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
  <tbody><tr>
    <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">&nbsp;</td>
    <td class="" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 540px; padding: 10px; width: 540px;">
      <div class="" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 600px; padding: 30px 20px;">
        <span class="" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Welcome back to Build SaaS with Ethan!</span>
        <table class="" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
          <tbody><tr>
            <td class="" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; box-sizing: border-box;">
              <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
                <tbody><tr>
                  <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
                    <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 20px; color: #15212A; font-weight: bold; line-height: 24px; margin: 0; margin-bottom: 15px;">${sendInfo.a}</p>
                    <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 24px; margin-bottom: 32px;">${sendInfo.b}</p>
                    <table border="0" cellpadding="0" cellspacing="0" class="" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
                      <tbody>
                        <tr>
                          <td align="left" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top; padding-bottom: 35px;">
                            <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
                              <tbody>
                                <tr>
                                  <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top; background-color: #3B82F6; border-radius: 5px; text-align: center;"> <p style="display: inline-block; color: #ffffff; background-color: #3B82F6; border: solid 1px #3B82F6; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 16px; font-weight: normal; margin: 0; padding: 9px 22px 10px; border-color: #3B82F6;" _act="check_domail">${sendInfo.hashedemail}</p> </td>
                                </tr>
                              </tbody>
                            </table>
                          </td>
                        </tr>
                      </tbody>
                    </table>
                    <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 24px; margin: 0; margin-bottom: 11px;">${sendInfo.c}</p>
                    <hr>
                  </td>
                </tr>
                <tr>
                  <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-top: 80px;">
                    <p class="" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 16px; font-size: 11px; color: #738A94; font-weight: normal; margin: 0;">${sendInfo.d}</p>
                  </td>
                </tr>
                <tr>
                  <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-top: 2px;">
                    <p class="" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 16px; font-size: 11px; color: #738A94; font-weight: normal; margin: 0;">${sendInfo.e}<a target="_blank" class="" href="${sendInfo.url}" style="text-decoration: underline; color: #738A94; font-size: 11px;" _act="check_domail">${sendInfo.domain}</a> ${sendInfo.f} <span style="text-decoration: underline; color: #738A94; font-size: 11px;" _act="check_domail">${sendInfo.sendEmail}</span></p>
                  </td>
                </tr>
              </tbody></table>
            </td>
          </tr>
        </tbody></table>
      </div>
    </td>
    <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">&nbsp;</td>
  </tr>
</tbody></table></div>
  `
}

存储邮件主题文案

ts 复制代码
// src/lib/tips.ts
const messages = {
  '9995': '恭喜你发现了宝藏',
  '9996': 'ITShareNotes官网注册',
  // ...
}
export const emailMessages = {
  SUBJECT: messages['9996'],
  TEXT: messages['9995'],
}

我们创建了邮件模板和存储邮件主题文案,在激活邮件方法中调用sendEmail

ts 复制代码
// 发送邮件
await sendEmail({
  to: email,
  subject: emailMessages.SUBJECT,
  text: emailMessages.TEXT,
  html: getEmailTemplate(hashedEmail, email),
})

五、英文邮箱

默认qq邮箱的前缀都是数字,他支持我们免费注册英文邮箱,注册后EMAIL_USER就可以使用英文邮箱进行配置,收到的邮件也是由英文邮箱发送过来的

相关推荐
潜意识起点3 分钟前
精通 CSS 阴影效果:从基础到高级应用
前端·css
奋斗吧程序媛7 分钟前
删除VSCode上 origin/分支名,但GitLab上实际上不存在的分支
前端·vscode
IT女孩儿17 分钟前
JavaScript--WebAPI查缺补漏(二)
开发语言·前端·javascript·html·ecmascript
m0_748256563 小时前
如何解决前端发送数据到后端为空的问题
前端
请叫我飞哥@3 小时前
HTML5适配手机
前端·html·html5
@解忧杂货铺4 小时前
前端vue如何实现数字框中通过鼠标滚轮上下滚动增减数字
前端·javascript·vue.js
F-2H6 小时前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++
gqkmiss7 小时前
Chrome 浏览器插件获取网页 iframe 中的 window 对象
前端·chrome·iframe·postmessage·chrome 插件
m0_748247559 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
m0_748255029 小时前
前端常用算法集合
前端·算法