From 5d3b0c78d818cbe4bc85295920d0729faf6fb06f Mon Sep 17 00:00:00 2001 From: Star Date: Fri, 1 Aug 2025 00:54:13 +0800 Subject: [PATCH] support imap --- go.mod | 42 +++--- imap.go | 370 +++++++++++++++++++++++++++++++++++++++++++++++++ mail.go | 65 +++++++++ mail_test.js | 31 +++++ plugin.go | 221 +++++++---------------------- plugin_test.go | 53 ++++++- plugin_test.js | 9 -- smtp.go | 135 ++++++++++++++++++ 8 files changed, 727 insertions(+), 199 deletions(-) create mode 100644 imap.go create mode 100644 mail.go create mode 100644 mail_test.js delete mode 100644 plugin_test.js create mode 100644 smtp.go diff --git a/go.mod b/go.mod index 894900d..d18faa3 100644 --- a/go.mod +++ b/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 ) diff --git a/imap.go b/imap.go new file mode 100644 index 0000000..64e7069 --- /dev/null +++ b/imap.go @@ -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 +} diff --git a/mail.go b/mail.go new file mode 100644 index 0000000..f06260d --- /dev/null +++ b/mail.go @@ -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() +} diff --git a/mail_test.js b/mail_test.js new file mode 100644 index 0000000..0082b31 --- /dev/null +++ b/mail_test.js @@ -0,0 +1,31 @@ +import mail from 'apigo.cc/gojs/mail' +import co from 'apigo.cc/gojs/console' + +// mail.send(['star3s@126.com'], '测试邮件', '这是一个测试邮件
!!', { +// 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 \ No newline at end of file diff --git a/plugin.go b/plugin.go index 065d681..dd5f22a 100644 --- a/plugin.go +++ b/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 -} diff --git a/plugin_test.go b/plugin_test.go index 2ff3b55..f2943aa 100644 --- a/plugin_test.go +++ b/plugin_test.go @@ -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 { diff --git a/plugin_test.js b/plugin_test.js deleted file mode 100644 index 7ac81e2..0000000 --- a/plugin_test.js +++ /dev/null @@ -1,9 +0,0 @@ -import mail from 'apigo.cc/gojs/mail' - -mail.send(['star3s@126.com'], '测试邮件', '这是一个测试邮件
!!', { - 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 \ No newline at end of file diff --git a/smtp.go b/smtp.go new file mode 100644 index 0000000..7390d71 --- /dev/null +++ b/smtp.go @@ -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 +}