From 0396d8222edf14a980b7cc6ab35265894a4b9ace Mon Sep 17 00:00:00 2001 From: Leo Chen <Leo@zhangbudademao.com> Date: Sun, 27 Oct 2024 20:21:34 +0800 Subject: [PATCH 1/2] feat: add mail push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增电子邮箱推送 --- internal/domain/notify.go | 1 + internal/notify/mail.go | 58 +++++ internal/notify/notify.go | 10 + ui/src/components/notify/Mail.tsx | 319 +++++++++++++++++++++++ ui/src/domain/settings.ts | 13 +- ui/src/i18n/locales/en/nls.common.json | 3 +- ui/src/i18n/locales/en/nls.settings.json | 6 + ui/src/i18n/locales/zh/nls.common.json | 3 +- ui/src/i18n/locales/zh/nls.settings.json | 6 + ui/src/pages/setting/Notify.tsx | 7 + 10 files changed, 423 insertions(+), 3 deletions(-) create mode 100644 internal/notify/mail.go create mode 100644 ui/src/components/notify/Mail.tsx diff --git a/internal/domain/notify.go b/internal/domain/notify.go index de9cf6cf..534d8b02 100644 --- a/internal/domain/notify.go +++ b/internal/domain/notify.go @@ -6,6 +6,7 @@ const ( NotifyChannelTelegram = "telegram" NotifyChannelLark = "lark" NotifyChannelServerChan = "serverchan" + NotifyChannelMail = "mail" ) type NotifyTestPushReq struct { diff --git a/internal/notify/mail.go b/internal/notify/mail.go new file mode 100644 index 00000000..ece00ad2 --- /dev/null +++ b/internal/notify/mail.go @@ -0,0 +1,58 @@ +package notify + +import ( + "context" + "net/smtp" +) + +type Mail struct { + senderAddress string + smtpHostAddr string + smtpHostPort string + smtpAuth smtp.Auth + receiverAddresses string +} + +func NewMail(senderAddress, receiverAddresses, smtpHostAddr, smtpHostPort string) *Mail { + if(smtpHostPort == "") { + smtpHostPort = "25" + } + + return &Mail{ + senderAddress: senderAddress, + smtpHostAddr: smtpHostAddr, + smtpHostPort: smtpHostPort, + receiverAddresses: receiverAddresses, + } +} + +func (m *Mail) SetAuth(username, password string) { + m.smtpAuth = smtp.PlainAuth("", username, password, m.smtpHostAddr) +} + +func (m *Mail) Send(ctx context.Context, subject, message string) error { + // 构建邮件 + from := m.senderAddress + to := []string{m.receiverAddresses} + msg := []byte( + "From: " + from + "\r\n" + + "To: " + m.receiverAddresses + "\r\n" + + "Subject: " + subject + "\r\n" + + "\r\n" + + message + "\r\n") + + var smtpAddress string + // 组装邮箱服务器地址 + if(m.smtpHostPort == "25"){ + smtpAddress = m.smtpHostAddr + }else{ + smtpAddress = m.smtpHostAddr + ":" + m.smtpHostPort + } + + err := smtp.SendMail(smtpAddress, m.smtpAuth, from, to, msg) + if err != nil { + return err + } + + return nil +} \ No newline at end of file diff --git a/internal/notify/notify.go b/internal/notify/notify.go index 3dfa643b..4b91cdbd 100644 --- a/internal/notify/notify.go +++ b/internal/notify/notify.go @@ -106,6 +106,8 @@ func getNotifier(channel string, conf map[string]any) (notifyPackage.Notifier, e return getWebhookNotifier(conf), nil case domain.NotifyChannelServerChan: return getServerChanNotifier(conf), nil + case domain.NotifyChannelMail: + return getMailNotifier(conf), nil } return nil, fmt.Errorf("notifier not found") @@ -166,6 +168,14 @@ func getLarkNotifier(conf map[string]any) notifyPackage.Notifier { return lark.NewWebhookService(getString(conf, "webhookUrl")) } +func getMailNotifier(conf map[string]any) notifyPackage.Notifier { + rs := NewMail(getString(conf, "senderAddress"),getString(conf,"receiverAddress"), getString(conf, "smtpHostAddr"), getString(conf, "smtpHostPort")) + + rs.SetAuth(getString(conf, "username"), getString(conf, "password")) + + return rs +} + func getString(conf map[string]any, key string) string { if _, ok := conf[key]; !ok { return "" diff --git a/ui/src/components/notify/Mail.tsx b/ui/src/components/notify/Mail.tsx new file mode 100644 index 00000000..9161ba09 --- /dev/null +++ b/ui/src/components/notify/Mail.tsx @@ -0,0 +1,319 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { useToast } from "@/components/ui/use-toast"; +import { getErrMessage } from "@/lib/error"; +import { NotifyChannelMail, NotifyChannels } from "@/domain/settings"; +import { useNotifyContext } from "@/providers/notify"; +import { update } from "@/repository/settings"; +import Show from "@/components/Show"; +import { notifyTest } from "@/api/notify"; + +type MailSetting = { + id: string; + name: string; + data: NotifyChannelMail; +}; + +const Mail = () => { + const { config, setChannels } = useNotifyContext(); + const { t } = useTranslation(); + + const [changed, setChanged] = useState<boolean>(false); + + const [mail, setmail] = useState<MailSetting>({ + id: config.id ?? "", + name: "notifyChannels", + data: { + senderAddress: "", + receiverAddresses: "", + smtpHostAddr: "", + smtpHostPort: "25", + username: "", + password: "", + enabled: false, + }, + }); + + const [originMail, setoriginMail] = useState<MailSetting>({ + id: config.id ?? "", + name: "notifyChannels", + data: { + senderAddress: "", + receiverAddresses: "", + smtpHostAddr: "", + smtpHostPort: "25", + username: "", + password: "", + enabled: false, + }, + }); + + useEffect(() => { + setChanged(false); + }, [config]); + + useEffect(() => { + const data = getDetailMail(); + setoriginMail({ + id: config.id ?? "", + name: "mail", + data, + }); + }, [config]); + + useEffect(() => { + const data = getDetailMail(); + setmail({ + id: config.id ?? "", + name: "mail", + data, + }); + }, [config]); + + const { toast } = useToast(); + + const getDetailMail = () => { + const df: NotifyChannelMail = { + senderAddress: "", + receiverAddresses: "", + smtpHostAddr: "", + smtpHostPort: "25", + username: "", + password: "", + enabled: false, + }; + if (!config.content) { + return df; + } + const chanels = config.content as NotifyChannels; + if (!chanels.mail) { + return df; + } + + return chanels.mail as NotifyChannelMail; + }; + + const checkChanged = (data: NotifyChannelMail) => { + if (data.senderAddress !== originMail.data.senderAddress || data.receiverAddresses !== originMail.data.receiverAddresses || data.smtpHostAddr !== originMail.data.smtpHostAddr || data.smtpHostPort !== originMail.data.smtpHostPort || data.username !== originMail.data.username || data.password !== originMail.data.password) { + setChanged(true); + } else { + setChanged(false); + } + }; + + const handleSaveClick = async () => { + try { + const resp = await update({ + ...config, + name: "notifyChannels", + content: { + ...config.content, + mail: { + ...mail.data, + }, + }, + }); + + setChannels(resp); + toast({ + title: t("common.save.succeeded.message"), + description: t("settings.notification.config.saved.message"), + }); + } catch (e) { + const msg = getErrMessage(e); + + toast({ + title: t("common.save.failed.message"), + description: `${t("settings.notification.config.failed.message")}: ${msg}`, + variant: "destructive", + }); + } + }; + + const handlePushTestClick = async () => { + try { + await notifyTest("mail"); + + toast({ + title: t("settings.notification.config.push.test.message.success.message"), + description: t("settings.notification.config.push.test.message.success.message"), + }); + } catch (e) { + const msg = getErrMessage(e); + + toast({ + title: t("settings.notification.config.push.test.message.failed.message"), + description: `${t("settings.notification.config.push.test.message.failed.message")}: ${msg}`, + variant: "destructive", + }); + } + }; + + const handleSwitchChange = async () => { + const newData = { + ...mail, + data: { + ...mail.data, + enabled: !mail.data.enabled, + }, + }; + setmail(newData); + + try { + const resp = await update({ + ...config, + name: "notifyChannels", + content: { + ...config.content, + mail: { + ...newData.data, + }, + }, + }); + + setChannels(resp); + } catch (e) { + const msg = getErrMessage(e); + + toast({ + title: t("common.save.failed.message"), + description: `${t("settings.notification.config.failed.message")}: ${msg}`, + variant: "destructive", + }); + } + }; + + return ( + <div> + <Input + placeholder={t("settings.notification.mail.sender_address.placeholder")} + value={mail.data.senderAddress} + onChange={(e) => { + const newData = { + ...mail, + data: { + ...mail.data, + senderAddress: e.target.value, + }, + }; + checkChanged(newData.data); + setmail(newData); + }} + /> + <Input + placeholder={t("settings.notification.mail.receiver_address.placeholder")} + className="mt-2" + value={mail.data.receiverAddresses} + onChange={(e) => { + const newData = { + ...mail, + data: { + ...mail.data, + secret: e.target.value, + }, + }; + checkChanged(newData.data); + setmail(newData); + }} + /> + <Input + placeholder={t("settings.notification.mail.smtp_host.placeholder")} + className="mt-2" + value={mail.data.smtpHostAddr} + onChange={(e) => { + const newData = { + ...mail, + data: { + ...mail.data, + secret: e.target.value, + }, + }; + checkChanged(newData.data); + setmail(newData); + }} + /> + <Input + placeholder={t("settings.notification.mail.smtp_port.placeholder")} + className="mt-2" + value={mail.data.smtpHostPort} + onChange={(e) => { + const newData = { + ...mail, + data: { + ...mail.data, + secret: e.target.value, + }, + }; + checkChanged(newData.data); + setmail(newData); + }} + /> + <Input + placeholder={t("settings.notification.mail.username.placeholder")} + className="mt-2" + value={mail.data.username} + onChange={(e) => { + const newData = { + ...mail, + data: { + ...mail.data, + secret: e.target.value, + }, + }; + checkChanged(newData.data); + setmail(newData); + }} + /> + <Input + placeholder={t("settings.notification.mail.password.placeholder")} + className="mt-2" + value={mail.data.password} + onChange={(e) => { + const newData = { + ...mail, + data: { + ...mail.data, + secret: e.target.value, + }, + }; + checkChanged(newData.data); + setmail(newData); + }} + /> + <div className="flex items-center space-x-1 mt-2"> + <Switch id="airplane-mode" checked={mail.data.enabled} onCheckedChange={handleSwitchChange} /> + <Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label> + </div> + + <div className="flex justify-end mt-2"> + <Show when={changed}> + <Button + onClick={() => { + handleSaveClick(); + }} + > + {t("common.save")} + </Button> + </Show> + + <Show when={!changed && mail.id != ""}> + <Button + variant="secondary" + onClick={() => { + handlePushTestClick(); + }} + > + {t("settings.notification.config.push.test.message")} + </Button> + </Show> + </div> + </div> + ); +}; + +export default Mail; diff --git a/ui/src/domain/settings.ts b/ui/src/domain/settings.ts index af77ea98..798c4e86 100644 --- a/ui/src/domain/settings.ts +++ b/ui/src/domain/settings.ts @@ -23,9 +23,10 @@ export type NotifyChannels = { telegram?: NotifyChannel; webhook?: NotifyChannel; serverchan?: NotifyChannel; + mail?: NotifyChannelMail; }; -export type NotifyChannel = NotifyChannelDingTalk | NotifyChannelLark | NotifyChannelTelegram | NotifyChannelWebhook | NotifyChannelServerChan; +export type NotifyChannel = NotifyChannelDingTalk | NotifyChannelLark | NotifyChannelTelegram | NotifyChannelWebhook | NotifyChannelServerChan | NotifyChannelMail; export type NotifyChannelDingTalk = { accessToken: string; @@ -54,6 +55,16 @@ export type NotifyChannelServerChan = { enabled: boolean; }; +export type NotifyChannelMail = { + senderAddress: string; + receiverAddresses: string; + smtpHostAddr: string; + smtpHostPort: string; + username:string; + password:string; + enabled: boolean; +}; + export const defaultNotifyTemplate: NotifyTemplate = { title: "您有 {COUNT} 张证书即将过期", content: "有 {COUNT} 张证书即将过期,域名分别为 {DOMAINS},请保持关注!", diff --git a/ui/src/i18n/locales/en/nls.common.json b/ui/src/i18n/locales/en/nls.common.json index d90008ac..6870c616 100644 --- a/ui/src/i18n/locales/en/nls.common.json +++ b/ui/src/i18n/locales/en/nls.common.json @@ -85,5 +85,6 @@ "common.provider.kubernetes.secret": "Kubernetes - Secret", "common.provider.dingtalk": "DingTalk", "common.provider.telegram": "Telegram", - "common.provider.lark": "Lark" + "common.provider.lark": "Lark", + "common.provider.mail": "Mail" } diff --git a/ui/src/i18n/locales/en/nls.settings.json b/ui/src/i18n/locales/en/nls.settings.json index b23f6ee0..fd84b888 100644 --- a/ui/src/i18n/locales/en/nls.settings.json +++ b/ui/src/i18n/locales/en/nls.settings.json @@ -36,6 +36,12 @@ "settings.notification.dingtalk.secret.placeholder": "Signature for signed addition", "settings.notification.url.errmsg.invalid": "Invalid Url format", "settings.notification.serverchan.url.placeholder": "Url, e.g. https://sctapi.ftqq.com/****************.send", + "settings.notification.mail.sender_address.placeholder": "Sender email address", + "settings.notification.mail.receiver_address.placeholder": "Receiver email address", + "settings.notification.mail.smtp_host.placeholder": "SMTP server address", + "settings.notification.mail.smtp_port.placeholder": "SMTP server port, if not set, default is 25", + "settings.notification.mail.username.placeholder": "username", + "settings.notification.mail.password.placeholder": "password", "settings.ca.tab": "Certificate Authority", "settings.ca.provider.errmsg.empty": "Please select a Certificate Authority", diff --git a/ui/src/i18n/locales/zh/nls.common.json b/ui/src/i18n/locales/zh/nls.common.json index d1144d79..12fcd58a 100644 --- a/ui/src/i18n/locales/zh/nls.common.json +++ b/ui/src/i18n/locales/zh/nls.common.json @@ -85,6 +85,7 @@ "common.provider.kubernetes.secret": "Kubernetes - Secret", "common.provider.dingtalk": "钉钉", "common.provider.telegram": "Telegram", - "common.provider.lark": "飞书" + "common.provider.lark": "飞书", + "common.provider.mail": "电子邮件" } diff --git a/ui/src/i18n/locales/zh/nls.settings.json b/ui/src/i18n/locales/zh/nls.settings.json index f3dedaa9..2c0a807f 100644 --- a/ui/src/i18n/locales/zh/nls.settings.json +++ b/ui/src/i18n/locales/zh/nls.settings.json @@ -36,6 +36,12 @@ "settings.notification.dingtalk.secret.placeholder": "加签的签名", "settings.notification.url.errmsg.invalid": "URL 格式不正确", "settings.notification.serverchan.url.placeholder": "Url, 形如: https://sctapi.ftqq.com/****************.send", + "settings.notification.mail.sender_address.placeholder": "发送邮箱地址", + "settings.notification.mail.receiver_address.placeholder": "接收邮箱地址", + "settings.notification.mail.smtp_host.placeholder": "SMTP服务器地址", + "settings.notification.mail.smtp_port.placeholder": "SMTP服务器端口, 如果未设置, 默认为25", + "settings.notification.mail.username.placeholder": "用于登录到邮件服务器的用户名", + "settings.notification.mail.password.placeholder": "用于登录到邮件服务器的密码", "settings.ca.tab": "证书颁发机构(CA)", "settings.ca.provider.errmsg.empty": "请选择证书分发机构", diff --git a/ui/src/pages/setting/Notify.tsx b/ui/src/pages/setting/Notify.tsx index ad06a082..350e4b8b 100644 --- a/ui/src/pages/setting/Notify.tsx +++ b/ui/src/pages/setting/Notify.tsx @@ -7,6 +7,7 @@ import NotifyTemplate from "@/components/notify/NotifyTemplate"; import Telegram from "@/components/notify/Telegram"; import Webhook from "@/components/notify/Webhook"; import ServerChan from "@/components/notify/ServerChan"; +import Mail from "@/components/notify/Mail"; import { NotifyProvider } from "@/providers/notify"; const Notify = () => { @@ -61,6 +62,12 @@ const Notify = () => { <ServerChan /> </AccordionContent> </AccordionItem> + <AccordionItem value="item-7" className="dark:border-stone-200"> + <AccordionTrigger>{t("common.provider.mail")}</AccordionTrigger> + <AccordionContent> + <Mail /> + </AccordionContent> + </AccordionItem> </Accordion> </div> </NotifyProvider> From c9f347f77af04aeb41b125afdde0a82f382bc445 Mon Sep 17 00:00:00 2001 From: Leo Chen <Leo@zhangbudademao.com> Date: Sun, 27 Oct 2024 20:27:46 +0800 Subject: [PATCH 2/2] fix mail push onchange --- ui/src/components/notify/Mail.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/src/components/notify/Mail.tsx b/ui/src/components/notify/Mail.tsx index 9161ba09..7b6195fb 100644 --- a/ui/src/components/notify/Mail.tsx +++ b/ui/src/components/notify/Mail.tsx @@ -214,7 +214,7 @@ const Mail = () => { ...mail, data: { ...mail.data, - secret: e.target.value, + receiverAddresses: e.target.value, }, }; checkChanged(newData.data); @@ -230,7 +230,7 @@ const Mail = () => { ...mail, data: { ...mail.data, - secret: e.target.value, + smtpHostAddr: e.target.value, }, }; checkChanged(newData.data); @@ -246,7 +246,7 @@ const Mail = () => { ...mail, data: { ...mail.data, - secret: e.target.value, + smtpHostPort: e.target.value, }, }; checkChanged(newData.data); @@ -262,7 +262,7 @@ const Mail = () => { ...mail, data: { ...mail.data, - secret: e.target.value, + username: e.target.value, }, }; checkChanged(newData.data); @@ -278,7 +278,7 @@ const Mail = () => { ...mail, data: { ...mail.data, - secret: e.target.value, + password: e.target.value, }, }; checkChanged(newData.data);