support imap

This commit is contained in:
Star 2025-08-01 00:54:13 +08:00
parent 182b2fd96f
commit 5d3b0c78d8
8 changed files with 727 additions and 199 deletions

42
go.mod
View File

@ -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
View 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
View 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
View 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
View File

@ -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
}

View File

@ -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 {

View File

@ -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
View 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
}