support imap
This commit is contained in:
parent
182b2fd96f
commit
cdd6e2968d
42
go.mod
42
go.mod
@ -1,34 +1,38 @@
|
||||
module apigo.cc/gojs/mail
|
||||
|
||||
go 1.18
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
apigo.cc/gojs v0.0.12
|
||||
apigo.cc/gojs v0.0.25
|
||||
apigo.cc/gojs/console v0.0.2
|
||||
apigo.cc/gojs/redis v0.0.2
|
||||
github.com/ssgo/u v1.7.13
|
||||
github.com/emersion/go-imap/v2 v2.0.0-beta.5
|
||||
github.com/emersion/go-message v0.18.1
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056
|
||||
github.com/ssgo/config v1.7.9
|
||||
github.com/ssgo/u v1.7.21
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/emersion/go-imap v1.2.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||
github.com/gomodule/redigo v1.9.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
|
||||
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect
|
||||
github.com/jhillyerd/enmime v1.3.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/ssgo/config v1.7.9 // indirect
|
||||
github.com/ssgo/log v1.7.7 // indirect
|
||||
github.com/ssgo/redis v1.7.7 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/ssgo/log v1.7.9 // indirect
|
||||
github.com/ssgo/standard v1.7.7 // indirect
|
||||
github.com/ssgo/tool v0.4.28 // indirect
|
||||
github.com/ssgo/tool v0.4.29 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
golang.org/x/net v0.32.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
370
imap.go
Normal file
370
imap.go
Normal file
@ -0,0 +1,370 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"apigo.cc/gojs"
|
||||
"apigo.cc/gojs/goja"
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/imapclient"
|
||||
"github.com/jhillyerd/enmime"
|
||||
"github.com/ssgo/u"
|
||||
)
|
||||
|
||||
type Mail struct {
|
||||
Uid uint32
|
||||
From []string
|
||||
To []string
|
||||
Cc []string
|
||||
Bcc []string
|
||||
Subject string
|
||||
Date string
|
||||
Text string
|
||||
Html string
|
||||
Attachments []Attachment
|
||||
Embeds []Attachment
|
||||
Size int64
|
||||
Flags []string
|
||||
}
|
||||
|
||||
// 接收选项
|
||||
type RecvOption struct {
|
||||
Tag uint32
|
||||
Mailbox string
|
||||
Unseen bool
|
||||
Read bool
|
||||
Answered bool
|
||||
Flagged bool
|
||||
Deleted bool
|
||||
Since string
|
||||
Before string
|
||||
Limit uint32
|
||||
MarkAsRead bool
|
||||
|
||||
Subject []string
|
||||
Search []string
|
||||
Body []string
|
||||
From []string
|
||||
To []string
|
||||
Cc []string
|
||||
Bcc []string
|
||||
Not []string
|
||||
}
|
||||
|
||||
type MailboxInfo struct {
|
||||
Name string
|
||||
Delimiter string
|
||||
Attrs []string
|
||||
}
|
||||
|
||||
type RecvResult struct {
|
||||
List []Mail
|
||||
Tag uint32
|
||||
}
|
||||
|
||||
type IMAPClient struct {
|
||||
client *imapclient.Client
|
||||
}
|
||||
|
||||
func (obj *IMAPClient) Close(vm *goja.Runtime) {
|
||||
obj.client.Logout().Wait()
|
||||
obj.client.Close()
|
||||
}
|
||||
|
||||
func (obj *Mailbox) OpenIMAP(vm *goja.Runtime) (*IMAPClient, error) {
|
||||
logger := gojs.GetLogger(vm)
|
||||
|
||||
// 连接服务器
|
||||
addr := fmt.Sprintf("%s:%d", obj.imapHost, obj.imapPort)
|
||||
imapClient, err := imapclient.DialTLS(addr, nil)
|
||||
if err != nil {
|
||||
logger.Error("failed to connect to IMAP server", "addr", addr, "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 发送客户端标识
|
||||
imapClient.ID(&imap.IDData{
|
||||
Name: conf.ClientName,
|
||||
Version: conf.ClientVersion,
|
||||
Vendor: conf.ClientVendor,
|
||||
SupportURL: conf.ClientSupportURL,
|
||||
OS: conf.ClientOS,
|
||||
})
|
||||
|
||||
// 登录
|
||||
if err := imapClient.Login(obj.username, obj.password).Wait(); err != nil {
|
||||
logger.Error("IMAP login failed", "user", obj.username, "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &IMAPClient{client: imapClient}, nil
|
||||
}
|
||||
|
||||
func (obj *IMAPClient) ListMailbox(vm *goja.Runtime) ([]MailboxInfo, error) {
|
||||
logger := gojs.GetLogger(vm)
|
||||
mailboxes, err := obj.client.List("", "%", nil).Collect()
|
||||
if err != nil {
|
||||
logger.Error("failed to list mailboxes", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
list := []MailboxInfo{}
|
||||
for _, mbox := range mailboxes {
|
||||
// fmt.Println(u.JsonP(mbox), 111)
|
||||
attrs := []string{}
|
||||
for _, attr := range mbox.Attrs {
|
||||
attrs = append(attrs, string(attr))
|
||||
}
|
||||
list = append(list, MailboxInfo{
|
||||
Name: mbox.Mailbox,
|
||||
Delimiter: string(mbox.Delim),
|
||||
Attrs: attrs,
|
||||
})
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (obj *IMAPClient) Recv(vm *goja.Runtime, opt *RecvOption) (*RecvResult, error) {
|
||||
logger := gojs.GetLogger(vm)
|
||||
|
||||
// 创建默认选项
|
||||
if opt == nil {
|
||||
opt = &RecvOption{
|
||||
Mailbox: "INBOX",
|
||||
Unseen: true,
|
||||
}
|
||||
}
|
||||
rr := &RecvResult{Tag: opt.Tag, List: []Mail{}}
|
||||
|
||||
// 设置默认限制
|
||||
if opt.Limit == 0 {
|
||||
opt.Limit = 20
|
||||
}
|
||||
|
||||
// 选择邮箱
|
||||
mailboxName := opt.Mailbox
|
||||
if mailboxName == "" {
|
||||
mailboxName = "INBOX"
|
||||
}
|
||||
|
||||
// 选择邮箱
|
||||
c := obj.client
|
||||
selectCmd := c.Select(mailboxName, nil)
|
||||
_, err := selectCmd.Wait()
|
||||
if err != nil {
|
||||
logger.Error("failed to select mailbox", "mailbox", mailboxName, "err", err)
|
||||
return rr, err
|
||||
}
|
||||
|
||||
limit := imap.UID(opt.Limit)
|
||||
startUID := imap.UID(opt.Tag) + 1 // 从上一次最大UID+1开始
|
||||
endUID := imap.UID(math.MaxUint32) // 始终使用最大值,确保搜索时能拿到足够的结果
|
||||
criteria := &imap.SearchCriteria{
|
||||
UID: []imap.UIDSet{[]imap.UIDRange{{Start: startUID, Stop: endUID}}},
|
||||
}
|
||||
|
||||
// 添加基本搜索条件
|
||||
if opt.Unseen {
|
||||
criteria.NotFlag = append(criteria.NotFlag, imap.FlagSeen)
|
||||
}
|
||||
if opt.Read {
|
||||
criteria.Flag = append(criteria.Flag, imap.FlagSeen)
|
||||
}
|
||||
if opt.Answered {
|
||||
criteria.Flag = append(criteria.Flag, imap.FlagAnswered)
|
||||
}
|
||||
if opt.Flagged {
|
||||
criteria.Flag = append(criteria.Flag, imap.FlagFlagged)
|
||||
}
|
||||
if opt.Deleted {
|
||||
criteria.Flag = append(criteria.Flag, imap.FlagDeleted)
|
||||
}
|
||||
|
||||
// 添加时间范围条件
|
||||
if opt.Since != "" {
|
||||
criteria.Since = u.ParseTime(opt.Since)
|
||||
}
|
||||
if opt.Before != "" {
|
||||
criteria.Before = u.ParseTime(opt.Before)
|
||||
}
|
||||
|
||||
// 添加自定义搜索条件
|
||||
if len(opt.Search) > 0 {
|
||||
criteria.Text = opt.Search
|
||||
}
|
||||
if len(opt.Body) > 0 {
|
||||
criteria.Body = opt.Body
|
||||
}
|
||||
|
||||
if len(opt.Subject) > 0 {
|
||||
for _, word := range opt.Subject {
|
||||
criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{
|
||||
Key: "Subject",
|
||||
Value: word,
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(opt.From) > 0 {
|
||||
for _, word := range opt.From {
|
||||
criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{
|
||||
Key: "From",
|
||||
Value: word,
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(opt.To) > 0 {
|
||||
for _, word := range opt.To {
|
||||
criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{
|
||||
Key: "To",
|
||||
Value: word,
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(opt.Cc) > 0 {
|
||||
for _, word := range opt.Cc {
|
||||
criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{
|
||||
Key: "Cc",
|
||||
Value: word,
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(opt.Bcc) > 0 {
|
||||
for _, word := range opt.Bcc {
|
||||
criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{
|
||||
Key: "Bcc",
|
||||
Value: word,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(opt.Not) > 0 {
|
||||
criteria.Not = []imap.SearchCriteria{}
|
||||
criteria.Not = append(criteria.Not, imap.SearchCriteria{
|
||||
Text: opt.Not,
|
||||
})
|
||||
}
|
||||
|
||||
r, err := c.UIDSearch(criteria, nil).Wait()
|
||||
if err != nil {
|
||||
logger.Error("failed to search messages", "err", err)
|
||||
return rr, err
|
||||
}
|
||||
|
||||
var uidSet imap.UIDSet
|
||||
maxUID := startUID
|
||||
var uidCount imap.UID
|
||||
if all, ok := r.All.(imap.UIDSet); ok {
|
||||
for _, set := range all {
|
||||
n := set.Stop - set.Start
|
||||
if uidCount+n > limit {
|
||||
set.Stop -= (uidCount + n - limit)
|
||||
}
|
||||
maxUID = set.Stop
|
||||
uidCount += set.Stop - set.Start
|
||||
uidSet = append(uidSet, set)
|
||||
if uidCount >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(uidSet) == 0 {
|
||||
return rr, nil
|
||||
}
|
||||
fr, err := c.Fetch(uidSet, &imap.FetchOptions{Envelope: true, BodySection: []*imap.FetchItemBodySection{{}}}).Collect()
|
||||
if err != nil {
|
||||
logger.Error("failed to fetch messages", "err", err)
|
||||
return rr, err
|
||||
}
|
||||
|
||||
for _, msg := range fr {
|
||||
mail := Mail{
|
||||
Uid: uint32(msg.UID),
|
||||
Subject: msg.Envelope.Subject,
|
||||
From: FormatAddressList(msg.Envelope.From),
|
||||
To: FormatAddressList(msg.Envelope.To),
|
||||
Cc: FormatAddressList(msg.Envelope.Cc),
|
||||
Bcc: FormatAddressList(msg.Envelope.Bcc),
|
||||
Date: msg.Envelope.Date.Format(time.DateTime),
|
||||
Size: msg.RFC822Size,
|
||||
Flags: FormatFlags(msg.Flags),
|
||||
}
|
||||
|
||||
// 解析邮件正文和附件
|
||||
// fmt.Println("####0", len(msg.BodySection))
|
||||
for _, section := range msg.BodySection {
|
||||
if section.Section != nil && section.Section.Specifier == imap.PartSpecifierNone {
|
||||
// fmt.Println("####1", len(section.Bytes))
|
||||
env, err := enmime.ReadEnvelope(bytes.NewReader(section.Bytes))
|
||||
if err == nil {
|
||||
mail.Text = env.Text
|
||||
mail.Html = env.HTML
|
||||
|
||||
// 处理附件
|
||||
for _, attach := range env.Attachments {
|
||||
mail.Attachments = append(mail.Attachments, Attachment{
|
||||
Name: attach.FileName,
|
||||
Data: u.Base64(attach.Content),
|
||||
ContentType: attach.ContentType,
|
||||
})
|
||||
}
|
||||
|
||||
// 处理内嵌资源
|
||||
for _, embed := range env.Inlines {
|
||||
mail.Embeds = append(mail.Embeds, Attachment{
|
||||
Name: embed.FileName,
|
||||
Data: u.Base64(embed.Content),
|
||||
ContentType: embed.ContentType,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
logger.Error("failed to read envelope", "err", err)
|
||||
return rr, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rr.List = append(rr.List, mail)
|
||||
}
|
||||
|
||||
rr.Tag = uint32(maxUID)
|
||||
return rr, nil
|
||||
}
|
||||
|
||||
func FormatFlags(flags []imap.Flag) []string {
|
||||
result := make([]string, len(flags))
|
||||
for i, flag := range flags {
|
||||
result[i] = string(flag)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ParseFlags(flags []string) []imap.Flag {
|
||||
result := make([]imap.Flag, len(flags))
|
||||
for i, flag := range flags {
|
||||
result[i] = imap.Flag(flag)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 地址格式化
|
||||
func FormatAddress(addr *imap.Address) string {
|
||||
if addr == nil {
|
||||
return ""
|
||||
}
|
||||
if addr.Name != "" {
|
||||
return fmt.Sprintf("%s <%s>", addr.Name, addr.Addr())
|
||||
}
|
||||
return addr.Addr()
|
||||
}
|
||||
|
||||
// 地址列表格式化
|
||||
func FormatAddressList(addrs []imap.Address) []string {
|
||||
result := make([]string, len(addrs))
|
||||
for i, addr := range addrs {
|
||||
result[i] = FormatAddress(&addr)
|
||||
}
|
||||
return result
|
||||
}
|
65
mail.go
Normal file
65
mail.go
Normal file
@ -0,0 +1,65 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"net/mail"
|
||||
|
||||
"apigo.cc/gojs/goja"
|
||||
)
|
||||
|
||||
type Mailbox struct {
|
||||
smtpHost string
|
||||
smtpPort int
|
||||
imapHost string
|
||||
imapPort int
|
||||
username string
|
||||
password string
|
||||
senderName string
|
||||
senderMail string
|
||||
}
|
||||
|
||||
type Attachment struct {
|
||||
Path string
|
||||
Name string
|
||||
Data string
|
||||
ContentType string
|
||||
}
|
||||
|
||||
func (obj *Mailbox) Recv(vm *goja.Runtime, opt *RecvOption) (*RecvResult, error) {
|
||||
c, err := obj.OpenIMAP(vm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer c.Close(vm)
|
||||
return c.Recv(vm, opt)
|
||||
}
|
||||
|
||||
func (obj *Mailbox) ParseAddr(addr string) (name, address string, err error) {
|
||||
r, err := mail.ParseAddress(addr)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return r.Name, r.Address, nil
|
||||
}
|
||||
|
||||
func (obj *Mailbox) ParseAddrList(addrs []string) ([]map[string]string, error) {
|
||||
result := make([]map[string]string, len(addrs))
|
||||
for i, addr := range addrs {
|
||||
name, address, err := obj.ParseAddr(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[i] = map[string]string{
|
||||
"name": name,
|
||||
"address": address,
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (obj *Mailbox) FormatAddr(name, address string) string {
|
||||
addr := &mail.Address{
|
||||
Name: name,
|
||||
Address: address,
|
||||
}
|
||||
return addr.String()
|
||||
}
|
31
mail_test.js
Normal file
31
mail_test.js
Normal file
@ -0,0 +1,31 @@
|
||||
import mail from 'apigo.cc/gojs/mail'
|
||||
import co from 'apigo.cc/gojs/console'
|
||||
|
||||
// mail.send(['star3s@126.com'], '测试邮件', '这是一个测试邮件<br/><img src="cid:1.png"/> !!', {
|
||||
// cc: ['isstar3s@126.com'],
|
||||
// html: true,
|
||||
// embeds: [{ name: '1.png', data: 'iVBORw0KGgoAAAANSUhEUgAAABEAAAATCAMAAABBexbDAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAFZaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2LjAuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Chle4QcAAABjUExURaOju6yiu6ajtqKlt7ijttenuaCmvcipuOezvKStwbW2yqSgudSsuNyyuaSpvZeasua3uqWjtcSwv9XR6MS80ei0ys3H3audsLKswruqu8qjvOKrxOKtudOwveTe8+i3xMXC2TWaOysAAADFSURBVBjTRc7tssMQEAZgqW8VFBVEmtz/VZ5dOZ2+M368j92BEMzjdecBmSAI/dEEIegdKM8bxLeT5xStqXOUign/4pyDQxYMitMG4xYgtoFooU0q2VQYYIyhmHx9PgmIMSk7bl2poMTWmOQcpJqSUrpyjBJg4AdqLqVk6NbyoUCWWluMh4X7NQSFD7YJYwylQljJwnrH+VVhDisJY51z2A+wsB4Sfth757s6T5i30DdPON/39/s85/3mvSfYIYdsL+ze/wG5Qw7KZj0DQwAAAABJRU5ErkJggg==' }],
|
||||
// })
|
||||
|
||||
// let imap = mail.openIMAP()
|
||||
// let boxes = imap.listMailbox()
|
||||
// co.info(boxes)
|
||||
// imap.close()
|
||||
|
||||
let r = mail.recv({
|
||||
limit: 1,
|
||||
// subject: ['审核'],
|
||||
from: ['测试工程师'],
|
||||
// search: ['腾讯云备案管局审核通过通知'],
|
||||
// not: ['审核'],
|
||||
})
|
||||
co.info(r)
|
||||
|
||||
// r = mail.recv({
|
||||
// limit: 5,
|
||||
// tag: r.tag,
|
||||
// })
|
||||
// co.info(r)
|
||||
|
||||
|
||||
return true
|
221
plugin.go
221
plugin.go
@ -1,40 +1,68 @@
|
||||
package plugin
|
||||
package mail
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"apigo.cc/gojs"
|
||||
"apigo.cc/gojs/goja"
|
||||
"github.com/jaytaylor/html2text"
|
||||
"github.com/ssgo/config"
|
||||
"github.com/ssgo/log"
|
||||
"github.com/ssgo/u"
|
||||
"gopkg.in/gomail.v2"
|
||||
)
|
||||
|
||||
const pluginName = "mail"
|
||||
|
||||
var defaultObjectName = "default"
|
||||
type Config struct {
|
||||
Mailbox map[string]*MailboxConfig
|
||||
ClientName string
|
||||
ClientVersion string
|
||||
ClientOS string
|
||||
ClientVendor string
|
||||
ClientSupportURL string
|
||||
}
|
||||
|
||||
type MailboxConfig struct {
|
||||
SmtpHost string
|
||||
SmtpPort int
|
||||
ImapHost string
|
||||
ImapPort int
|
||||
Username string
|
||||
Password string
|
||||
SenderName string
|
||||
SenderMail string
|
||||
}
|
||||
|
||||
var conf = &Config{
|
||||
Mailbox: make(map[string]*MailboxConfig),
|
||||
ClientName: "Apigo Mail Client",
|
||||
ClientVersion: "1.0.0",
|
||||
ClientOS: runtime.GOOS,
|
||||
ClientVendor: "apigo.cc",
|
||||
ClientSupportURL: "https://apigo.cc/gojs/mail",
|
||||
}
|
||||
|
||||
var defaultObject = &DefaultObject{}
|
||||
var confAes *u.Aes = u.NewAes([]byte("?GQ$0K0GgLdO=f+~L68PLm$uhKr4'=tV"), []byte("VFs7@sK61cj^f?HZ"))
|
||||
var objects = make(map[string]*Object)
|
||||
var objects = make(map[string]*Mailbox)
|
||||
|
||||
func init() {
|
||||
tsCode := gojs.MakeTSCode(defaultObject)
|
||||
gojs.Register("apigo.cc/gojs/"+pluginName, gojs.Module{
|
||||
ObjectMaker: func(vm *goja.Runtime) gojs.Map {
|
||||
configs := make(map[string]*Config)
|
||||
config.LoadConfig(pluginName, &configs)
|
||||
for name, object := range configs {
|
||||
objects[name] = defaultObject.New(*object)
|
||||
config.LoadConfig(pluginName, conf)
|
||||
for name, object := range conf.Mailbox {
|
||||
objects[name] = defaultObject.New(object)
|
||||
}
|
||||
if defaultMailbox, err := defaultObject.Get("default"); err == nil {
|
||||
defaultObject.Mailbox = defaultMailbox
|
||||
} else if len(objects) == 0 {
|
||||
log.DefaultLogger.Warning("no mailbox configured", "example for env.yml", "mail:\n mailbox:\n default:\n username: ****@***.***")
|
||||
}
|
||||
defaultObject.Object = defaultObject.GetInstance("default")
|
||||
return gojs.ToMap(defaultObject)
|
||||
},
|
||||
TsCode: tsCode,
|
||||
Desc: "mail api by github.com/ssgo/mail",
|
||||
Desc: "mail api",
|
||||
SetSSKey: func(key, iv []byte) {
|
||||
confAes = u.NewAes(key, iv)
|
||||
},
|
||||
@ -42,18 +70,18 @@ func init() {
|
||||
}
|
||||
|
||||
type DefaultObject struct {
|
||||
*Object
|
||||
*Mailbox
|
||||
}
|
||||
|
||||
func (obj *DefaultObject) GetInstance(name string) *Object {
|
||||
func (obj *DefaultObject) Get(name string) (*Mailbox, error) {
|
||||
if o, ok := objects[name]; ok {
|
||||
return o
|
||||
return o, nil
|
||||
} else {
|
||||
return obj.Object
|
||||
return nil, gojs.Err("mailbox [" + name + "] not configured")
|
||||
}
|
||||
}
|
||||
|
||||
func (obj *DefaultObject) New(conf Config) *Object {
|
||||
func (obj *DefaultObject) New(conf *MailboxConfig) *Mailbox {
|
||||
a := strings.SplitN(conf.Username, "@", 2)
|
||||
if len(a) == 2 {
|
||||
if conf.SmtpHost == "" {
|
||||
@ -69,8 +97,11 @@ func (obj *DefaultObject) New(conf Config) *Object {
|
||||
conf.ImapPort = 993
|
||||
}
|
||||
}
|
||||
if conf.SenderMail == "" {
|
||||
conf.SenderMail = conf.Username
|
||||
}
|
||||
|
||||
return &Object{
|
||||
return &Mailbox{
|
||||
smtpHost: conf.SmtpHost,
|
||||
smtpPort: conf.SmtpPort,
|
||||
imapHost: conf.ImapHost,
|
||||
@ -78,154 +109,6 @@ func (obj *DefaultObject) New(conf Config) *Object {
|
||||
username: conf.Username,
|
||||
password: confAes.DecryptUrlBase64ToString(conf.Password),
|
||||
senderName: conf.SenderName,
|
||||
senderMail: conf.SenderMail,
|
||||
}
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
SmtpHost string
|
||||
SmtpPort int
|
||||
ImapHost string
|
||||
ImapPort int
|
||||
Username string
|
||||
Password string
|
||||
SenderName string
|
||||
}
|
||||
|
||||
type Object struct {
|
||||
smtpHost string
|
||||
smtpPort int
|
||||
imapHost string
|
||||
imapPort int
|
||||
username string
|
||||
password string
|
||||
senderName string
|
||||
}
|
||||
|
||||
type Attachment struct {
|
||||
Path string
|
||||
Name string
|
||||
Data string
|
||||
ContentType string
|
||||
}
|
||||
|
||||
type SendOption struct {
|
||||
Cc []string
|
||||
Bcc []string
|
||||
Html bool
|
||||
Attachments []Attachment
|
||||
Embeds []Attachment
|
||||
}
|
||||
|
||||
func (obj *Object) Send(vm *goja.Runtime, to []string, subject, content string, option *SendOption) error {
|
||||
logger := gojs.GetLogger(vm)
|
||||
|
||||
msg := gomail.NewMessage()
|
||||
if obj.senderName != "" {
|
||||
msg.SetHeader("From", msg.FormatAddress(obj.username, obj.senderName))
|
||||
} else {
|
||||
msg.SetHeader("From", obj.username)
|
||||
}
|
||||
|
||||
msg.SetHeader("To", to...)
|
||||
msg.SetHeader("Subject", subject)
|
||||
useHtml := false
|
||||
if option != nil {
|
||||
if len(option.Cc) > 0 {
|
||||
msg.SetHeader("Cc", option.Cc...)
|
||||
}
|
||||
if len(option.Bcc) > 0 {
|
||||
msg.SetHeader("Bcc", option.Bcc...)
|
||||
}
|
||||
if len(option.Attachments) > 0 {
|
||||
for _, attachment := range option.Attachments {
|
||||
if attachment.Path != "" {
|
||||
if u.FileExists(attachment.Path) {
|
||||
msg.Attach(attachment.Path)
|
||||
} else {
|
||||
logger.Error("mail attachment file not exists", "filename", attachment.Path, "to", to, "subject", subject)
|
||||
}
|
||||
} else if attachment.Data != "" {
|
||||
if attachment.Name == "" {
|
||||
attachment.Name = "attachment"
|
||||
}
|
||||
if attachment.ContentType == "" {
|
||||
attachment.ContentType = "text/plain"
|
||||
}
|
||||
buf, err := base64.StdEncoding.DecodeString(attachment.Data)
|
||||
if err != nil {
|
||||
buf = []byte(attachment.Data)
|
||||
}
|
||||
|
||||
msg.Attach(attachment.Name, gomail.SetCopyFunc(func(w io.Writer) error {
|
||||
_, err := w.Write(buf)
|
||||
return err
|
||||
}), gomail.SetHeader(map[string][]string{
|
||||
"Content-Type": {attachment.ContentType},
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(option.Embeds) > 0 {
|
||||
for _, attachment := range option.Embeds {
|
||||
if attachment.Path != "" {
|
||||
if u.FileExists(attachment.Path) {
|
||||
msg.Attach(attachment.Path)
|
||||
} else {
|
||||
logger.Error("mail embed file not exists", "filename", attachment.Path, "to", to, "subject", subject)
|
||||
}
|
||||
} else if attachment.Data != "" {
|
||||
if attachment.Name == "" {
|
||||
attachment.Name = "image"
|
||||
}
|
||||
if attachment.ContentType == "" {
|
||||
attachment.ContentType = "image/png"
|
||||
}
|
||||
buf, err := base64.StdEncoding.DecodeString(attachment.Data)
|
||||
if err != nil {
|
||||
buf = []byte(attachment.Data)
|
||||
}
|
||||
|
||||
msg.Embed(attachment.Name, gomail.SetCopyFunc(func(w io.Writer) error {
|
||||
_, err := w.Write(buf)
|
||||
return err
|
||||
}), gomail.SetHeader(map[string][]string{
|
||||
"Content-Type": {attachment.ContentType},
|
||||
"Content-ID": {"<" + attachment.Name + ">"},
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
useHtml = option.Html
|
||||
}
|
||||
if useHtml {
|
||||
msg.SetBody("text/html", content)
|
||||
textContent, err := html2text.FromString(content, html2text.Options{TextOnly: true, PrettyTables: true})
|
||||
if err != nil {
|
||||
logger.Error("convert html to text failed", "err", err, "to", to, "subject", subject, "content", content)
|
||||
textContent = content
|
||||
}
|
||||
msg.AddAlternative("text/plain", textContent)
|
||||
} else {
|
||||
msg.SetBody("text/plain", content)
|
||||
}
|
||||
|
||||
conn := gomail.NewDialer(obj.smtpHost, obj.smtpPort, obj.username, obj.password)
|
||||
switch obj.smtpPort {
|
||||
case 465:
|
||||
conn.SSL = true
|
||||
case 587:
|
||||
conn.TLSConfig = &tls.Config{
|
||||
ServerName: obj.smtpHost,
|
||||
InsecureSkipVerify: false,
|
||||
}
|
||||
}
|
||||
// conn.Auth = smtp.PlainAuth("", obj.username, obj.password, obj.smtpHost)
|
||||
|
||||
err := conn.DialAndSend(msg)
|
||||
if err != nil {
|
||||
logger.Error("send mail failed", "err", err.Error(), "to", to, "subject", "from", obj.username, subject, "content", content)
|
||||
} else {
|
||||
logger.Info("send mail success", "to", to, "from", obj.username, "subject", subject)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
@ -1,16 +1,65 @@
|
||||
package plugin_test
|
||||
package mail_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"apigo.cc/gojs"
|
||||
_ "apigo.cc/gojs/console"
|
||||
"github.com/ssgo/u"
|
||||
)
|
||||
|
||||
func TestPlugin(t *testing.T) {
|
||||
// c, err := imapclient.DialTLS("imap.126.com:993", nil)
|
||||
// if err != nil {
|
||||
// log.Fatalf("failed to dial IMAP server: %v", err)
|
||||
// }
|
||||
// defer c.Close()
|
||||
|
||||
// c.ID(&imap.IDData{
|
||||
// Name: "Apigo Mail Client",
|
||||
// Version: "1.0.0",
|
||||
// Vendor: "apigo.cc",
|
||||
// SupportURL: "https://apigo.cc/gojs/mail",
|
||||
// OS: runtime.GOOS,
|
||||
// })
|
||||
|
||||
// if err := c.Login("api_go@126.com", "ZZEQWMBONBXZGVKY").Wait(); err != nil {
|
||||
// log.Fatalf("failed to login: %v", err)
|
||||
// }
|
||||
|
||||
// mailboxes, err := c.List("", "%", nil).Collect()
|
||||
// if err != nil {
|
||||
// log.Fatalf("failed to list mailboxes: %v", err)
|
||||
// }
|
||||
// log.Printf("Found %v mailboxes", len(mailboxes))
|
||||
// for _, mbox := range mailboxes {
|
||||
// log.Printf(" - %v", mbox.Mailbox)
|
||||
// }
|
||||
|
||||
// selectedMbox, err := c.Select("INBOX", nil).Wait()
|
||||
// if err != nil {
|
||||
// log.Fatalf("failed to select INBOX: %v", err)
|
||||
// }
|
||||
// log.Printf("INBOX contains %v messages", selectedMbox.NumMessages)
|
||||
|
||||
// if selectedMbox.NumMessages > 0 {
|
||||
// seqSet := imap.SeqSetNum(1)
|
||||
// fetchOptions := &imap.FetchOptions{Envelope: true}
|
||||
// messages, err := c.Fetch(seqSet, fetchOptions).Collect()
|
||||
// if err != nil {
|
||||
// log.Fatalf("failed to fetch first message in INBOX: %v", err)
|
||||
// }
|
||||
// log.Printf("subject of first message in INBOX: %v", messages[0].Envelope.Subject)
|
||||
// }
|
||||
|
||||
// if err := c.Logout().Wait(); err != nil {
|
||||
// log.Fatalf("failed to logout: %v", err)
|
||||
// }
|
||||
|
||||
// return
|
||||
gojs.ExportForDev()
|
||||
r, err := gojs.RunFile("plugin_test.js")
|
||||
r, err := gojs.RunFile("mail_test.js")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if r != true {
|
||||
|
@ -1,9 +0,0 @@
|
||||
import mail from 'apigo.cc/gojs/mail'
|
||||
|
||||
mail.send(['star3s@126.com'], '测试邮件', '这是一个测试邮件<br/><img src="cid:1.png"/> !!', {
|
||||
cc: ['isstar3s@126.com'],
|
||||
html: true,
|
||||
embeds: [{ name: '1.png', data: 'iVBORw0KGgoAAAANSUhEUgAAABEAAAATCAMAAABBexbDAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAFZaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2LjAuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Chle4QcAAABjUExURaOju6yiu6ajtqKlt7ijttenuaCmvcipuOezvKStwbW2yqSgudSsuNyyuaSpvZeasua3uqWjtcSwv9XR6MS80ei0ys3H3audsLKswruqu8qjvOKrxOKtudOwveTe8+i3xMXC2TWaOysAAADFSURBVBjTRc7tssMQEAZgqW8VFBVEmtz/VZ5dOZ2+M368j92BEMzjdecBmSAI/dEEIegdKM8bxLeT5xStqXOUign/4pyDQxYMitMG4xYgtoFooU0q2VQYYIyhmHx9PgmIMSk7bl2poMTWmOQcpJqSUrpyjBJg4AdqLqVk6NbyoUCWWluMh4X7NQSFD7YJYwylQljJwnrH+VVhDisJY51z2A+wsB4Sfth757s6T5i30DdPON/39/s85/3mvSfYIYdsL+ze/wG5Qw7KZj0DQwAAAABJRU5ErkJggg==' }],
|
||||
})
|
||||
|
||||
return true
|
135
smtp.go
Normal file
135
smtp.go
Normal file
@ -0,0 +1,135 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
|
||||
"apigo.cc/gojs"
|
||||
"apigo.cc/gojs/goja"
|
||||
"github.com/jaytaylor/html2text"
|
||||
"github.com/ssgo/u"
|
||||
"gopkg.in/gomail.v2"
|
||||
)
|
||||
|
||||
type SendOption struct {
|
||||
Cc []string
|
||||
Bcc []string
|
||||
Html bool
|
||||
Attachments []Attachment
|
||||
Embeds []Attachment
|
||||
}
|
||||
|
||||
func (obj *Mailbox) Send(vm *goja.Runtime, to []string, subject, content string, option *SendOption) error {
|
||||
logger := gojs.GetLogger(vm)
|
||||
|
||||
msg := gomail.NewMessage()
|
||||
if obj.senderName != "" {
|
||||
msg.SetHeader("From", msg.FormatAddress(obj.username, obj.senderName))
|
||||
} else {
|
||||
msg.SetHeader("From", obj.username)
|
||||
}
|
||||
|
||||
msg.SetHeader("To", to...)
|
||||
msg.SetHeader("Subject", subject)
|
||||
useHtml := false
|
||||
if option != nil {
|
||||
if len(option.Cc) > 0 {
|
||||
msg.SetHeader("Cc", option.Cc...)
|
||||
}
|
||||
if len(option.Bcc) > 0 {
|
||||
msg.SetHeader("Bcc", option.Bcc...)
|
||||
}
|
||||
if len(option.Attachments) > 0 {
|
||||
for _, attachment := range option.Attachments {
|
||||
if attachment.Path != "" {
|
||||
if u.FileExists(attachment.Path) {
|
||||
msg.Attach(attachment.Path)
|
||||
} else {
|
||||
logger.Error("mail attachment file not exists", "filename", attachment.Path, "to", to, "subject", subject)
|
||||
}
|
||||
} else if attachment.Data != "" {
|
||||
if attachment.Name == "" {
|
||||
attachment.Name = "attachment"
|
||||
}
|
||||
if attachment.ContentType == "" {
|
||||
attachment.ContentType = "text/plain"
|
||||
}
|
||||
buf, err := base64.StdEncoding.DecodeString(attachment.Data)
|
||||
if err != nil {
|
||||
buf = []byte(attachment.Data)
|
||||
}
|
||||
|
||||
msg.Attach(attachment.Name, gomail.SetCopyFunc(func(w io.Writer) error {
|
||||
_, err := w.Write(buf)
|
||||
return err
|
||||
}), gomail.SetHeader(map[string][]string{
|
||||
"Content-Type": {attachment.ContentType},
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(option.Embeds) > 0 {
|
||||
for _, attachment := range option.Embeds {
|
||||
if attachment.Path != "" {
|
||||
if u.FileExists(attachment.Path) {
|
||||
msg.Attach(attachment.Path)
|
||||
} else {
|
||||
logger.Error("mail embed file not exists", "filename", attachment.Path, "to", to, "subject", subject)
|
||||
}
|
||||
} else if attachment.Data != "" {
|
||||
if attachment.Name == "" {
|
||||
attachment.Name = "image"
|
||||
}
|
||||
if attachment.ContentType == "" {
|
||||
attachment.ContentType = "image/png"
|
||||
}
|
||||
buf, err := base64.StdEncoding.DecodeString(attachment.Data)
|
||||
if err != nil {
|
||||
buf = []byte(attachment.Data)
|
||||
}
|
||||
|
||||
msg.Embed(attachment.Name, gomail.SetCopyFunc(func(w io.Writer) error {
|
||||
_, err := w.Write(buf)
|
||||
return err
|
||||
}), gomail.SetHeader(map[string][]string{
|
||||
"Content-Type": {attachment.ContentType},
|
||||
"Content-ID": {"<" + attachment.Name + ">"},
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
useHtml = option.Html
|
||||
}
|
||||
if useHtml {
|
||||
msg.SetBody("text/html", content)
|
||||
textContent, err := html2text.FromString(content, html2text.Options{TextOnly: true, PrettyTables: true})
|
||||
if err != nil {
|
||||
logger.Error("convert html to text failed", "err", err, "to", to, "subject", subject, "content", content)
|
||||
textContent = content
|
||||
}
|
||||
msg.AddAlternative("text/plain", textContent)
|
||||
} else {
|
||||
msg.SetBody("text/plain", content)
|
||||
}
|
||||
|
||||
conn := gomail.NewDialer(obj.smtpHost, obj.smtpPort, obj.username, obj.password)
|
||||
switch obj.smtpPort {
|
||||
case 465:
|
||||
conn.SSL = true
|
||||
case 587:
|
||||
conn.TLSConfig = &tls.Config{
|
||||
ServerName: obj.smtpHost,
|
||||
InsecureSkipVerify: false,
|
||||
}
|
||||
}
|
||||
// conn.Auth = smtp.PlainAuth("", obj.username, obj.password, obj.smtpHost)
|
||||
|
||||
err := conn.DialAndSend(msg)
|
||||
if err != nil {
|
||||
logger.Error("send mail failed", "err", err.Error(), "to", to, "subject", "from", obj.username, subject, "content", content)
|
||||
} else {
|
||||
logger.Info("send mail success", "to", to, "from", obj.username, "subject", subject)
|
||||
}
|
||||
return err
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user