feat: add Docker deployment and Microsoft OAuth support
This commit is contained in:
@@ -1,85 +1,139 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"mengpost-backend/database"
|
||||
)
|
||||
|
||||
// Account 代表一个邮箱账户的完整连接信息.
|
||||
type Account struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password,omitempty"`
|
||||
SMTPHost string `json:"smtp_host"`
|
||||
SMTPPort int `json:"smtp_port"`
|
||||
IMAPHost string `json:"imap_host"`
|
||||
IMAPPort int `json:"imap_port"`
|
||||
UseTLS bool `json:"use_tls"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func ListAccounts() ([]Account, error) {
|
||||
rows, err := database.DB.Query(`SELECT id,name,email,smtp_host,smtp_port,imap_host,imap_port,use_tls,created_at FROM accounts ORDER BY id DESC`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []Account
|
||||
for rows.Next() {
|
||||
var a Account
|
||||
var tls int
|
||||
if err := rows.Scan(&a.ID, &a.Name, &a.Email, &a.SMTPHost, &a.SMTPPort, &a.IMAPHost, &a.IMAPPort, &tls, &a.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.UseTLS = tls == 1
|
||||
out = append(out, a)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func GetAccount(id int64) (*Account, error) {
|
||||
row := database.DB.QueryRow(`SELECT id,name,email,password,smtp_host,smtp_port,imap_host,imap_port,use_tls,created_at FROM accounts WHERE id=?`, id)
|
||||
var a Account
|
||||
var tls int
|
||||
if err := row.Scan(&a.ID, &a.Name, &a.Email, &a.Password, &a.SMTPHost, &a.SMTPPort, &a.IMAPHost, &a.IMAPPort, &tls, &a.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.UseTLS = tls == 1
|
||||
return &a, nil
|
||||
}
|
||||
|
||||
func CreateAccount(a *Account) error {
|
||||
tls := 0
|
||||
if a.UseTLS {
|
||||
tls = 1
|
||||
}
|
||||
res, err := database.DB.Exec(
|
||||
`INSERT INTO accounts(name,email,password,smtp_host,smtp_port,imap_host,imap_port,use_tls) VALUES(?,?,?,?,?,?,?,?)`,
|
||||
a.Name, a.Email, a.Password, a.SMTPHost, a.SMTPPort, a.IMAPHost, a.IMAPPort, tls,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := res.LastInsertId()
|
||||
a.ID = id
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateAccount(a *Account) error {
|
||||
tls := 0
|
||||
if a.UseTLS {
|
||||
tls = 1
|
||||
}
|
||||
_, err := database.DB.Exec(
|
||||
`UPDATE accounts SET name=?,email=?,password=COALESCE(NULLIF(?, ''), password),smtp_host=?,smtp_port=?,imap_host=?,imap_port=?,use_tls=? WHERE id=?`,
|
||||
a.Name, a.Email, a.Password, a.SMTPHost, a.SMTPPort, a.IMAPHost, a.IMAPPort, tls, a.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteAccount(id int64) error {
|
||||
_, err := database.DB.Exec(`DELETE FROM accounts WHERE id=?`, id)
|
||||
return err
|
||||
}
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"mengpost-backend/database"
|
||||
)
|
||||
|
||||
// Account 邮箱账户(basic 为密码;oauth_microsoft 使用刷新令牌).
|
||||
type Account struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password,omitempty"`
|
||||
SMTPHost string `json:"smtp_host"`
|
||||
SMTPPort int `json:"smtp_port"`
|
||||
IMAPHost string `json:"imap_host"`
|
||||
IMAPPort int `json:"imap_port"`
|
||||
UseTLS bool `json:"use_tls"`
|
||||
AuthType string `json:"auth_type"`
|
||||
OAuthRefreshToken string `json:"-"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func readAuthType(ns sql.NullString) string {
|
||||
if ns.Valid && ns.String != "" {
|
||||
return ns.String
|
||||
}
|
||||
return AuthTypeBasic
|
||||
}
|
||||
|
||||
func ListAccounts() ([]Account, error) {
|
||||
rows, err := database.DB.Query(`
|
||||
SELECT id,name,email,smtp_host,smtp_port,imap_host,imap_port,use_tls,
|
||||
IFNULL(auth_type,'basic'), created_at
|
||||
FROM accounts ORDER BY id DESC`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []Account
|
||||
for rows.Next() {
|
||||
var a Account
|
||||
var tls int
|
||||
var authType sql.NullString
|
||||
if err := rows.Scan(&a.ID, &a.Name, &a.Email, &a.SMTPHost, &a.SMTPPort, &a.IMAPHost, &a.IMAPPort, &tls, &authType, &a.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.UseTLS = tls == 1
|
||||
a.AuthType = readAuthType(authType)
|
||||
out = append(out, a)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func scanFullAccount(row *sql.Row) (*Account, error) {
|
||||
var a Account
|
||||
var tls int
|
||||
var authType, oauthRefresh sql.NullString
|
||||
err := row.Scan(&a.ID, &a.Name, &a.Email, &a.Password, &a.SMTPHost, &a.SMTPPort, &a.IMAPHost, &a.IMAPPort, &tls, &authType, &oauthRefresh, &a.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.UseTLS = tls == 1
|
||||
a.AuthType = readAuthType(authType)
|
||||
if oauthRefresh.Valid {
|
||||
a.OAuthRefreshToken = oauthRefresh.String
|
||||
}
|
||||
return &a, nil
|
||||
}
|
||||
|
||||
func GetAccount(id int64) (*Account, error) {
|
||||
row := database.DB.QueryRow(`
|
||||
SELECT id,name,email,password,smtp_host,smtp_port,imap_host,imap_port,use_tls,
|
||||
IFNULL(auth_type,'basic'), oauth_refresh_token, created_at
|
||||
FROM accounts WHERE id=?`, id)
|
||||
return scanFullAccount(row)
|
||||
}
|
||||
|
||||
func GetAccountByEmail(email string) (*Account, error) {
|
||||
row := database.DB.QueryRow(`
|
||||
SELECT id,name,email,password,smtp_host,smtp_port,imap_host,imap_port,use_tls,
|
||||
IFNULL(auth_type,'basic'), oauth_refresh_token, created_at
|
||||
FROM accounts WHERE email=?`, email)
|
||||
return scanFullAccount(row)
|
||||
}
|
||||
|
||||
func CreateAccount(a *Account) error {
|
||||
if a.AuthType == "" {
|
||||
a.AuthType = AuthTypeBasic
|
||||
}
|
||||
tls := 0
|
||||
if a.UseTLS {
|
||||
tls = 1
|
||||
}
|
||||
var oauth any
|
||||
if a.OAuthRefreshToken != "" {
|
||||
oauth = a.OAuthRefreshToken
|
||||
}
|
||||
res, err := database.DB.Exec(
|
||||
`INSERT INTO accounts(name,email,password,smtp_host,smtp_port,imap_host,imap_port,use_tls,auth_type,oauth_refresh_token)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?)`,
|
||||
a.Name, a.Email, a.Password, a.SMTPHost, a.SMTPPort, a.IMAPHost, a.IMAPPort, tls, a.AuthType, oauth,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := res.LastInsertId()
|
||||
a.ID = id
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateAccount(a *Account) error {
|
||||
tls := 0
|
||||
if a.UseTLS {
|
||||
tls = 1
|
||||
}
|
||||
if a.AuthType == "" {
|
||||
a.AuthType = AuthTypeBasic
|
||||
}
|
||||
_, err := database.DB.Exec(
|
||||
`UPDATE accounts SET name=?,email=?,password=COALESCE(NULLIF(?, ''), password),smtp_host=?,smtp_port=?,imap_host=?,imap_port=?,use_tls=?,auth_type=?,
|
||||
oauth_refresh_token=COALESCE(NULLIF(?, ''), oauth_refresh_token) WHERE id=?`,
|
||||
a.Name, a.Email, a.Password, a.SMTPHost, a.SMTPPort, a.IMAPHost, a.IMAPPort, tls, a.AuthType,
|
||||
a.OAuthRefreshToken, a.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func UpdateAccountOAuthRefresh(id int64, refresh string) error {
|
||||
_, err := database.DB.Exec(`UPDATE accounts SET oauth_refresh_token=? WHERE id=?`, refresh, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteAccount(id int64) error {
|
||||
_, err := database.DB.Exec(`DELETE FROM accounts WHERE id=?`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
6
mengpost-backend/models/auth.go
Normal file
6
mengpost-backend/models/auth.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package models
|
||||
|
||||
const (
|
||||
AuthTypeBasic = "basic"
|
||||
AuthTypeOAuthMicrosoft = "oauth_microsoft"
|
||||
)
|
||||
47
mengpost-backend/models/oauth_pending.go
Normal file
47
mengpost-backend/models/oauth_pending.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"mengpost-backend/database"
|
||||
)
|
||||
|
||||
// ErrOAuthPendingNotFound state 不存在或尚未完成微软回调.
|
||||
var ErrOAuthPendingNotFound = errors.New("oauth state 无效或已过期")
|
||||
|
||||
func InsertOAuthPendingState(state string) error {
|
||||
_, err := database.DB.Exec(`INSERT INTO oauth_microsoft_pending(state) VALUES(?)`, state)
|
||||
return err
|
||||
}
|
||||
|
||||
func UpdateOAuthPendingTokens(state, refresh, email string) error {
|
||||
res, err := database.DB.Exec(
|
||||
`UPDATE oauth_microsoft_pending SET refresh_token=?, email=? WHERE state=?`,
|
||||
refresh, email, state,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
return ErrOAuthPendingNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TakeOAuthPending 取出并删除 pending,用于完成账户创建.
|
||||
func TakeOAuthPending(state string) (refresh, email string, err error) {
|
||||
row := database.DB.QueryRow(
|
||||
`SELECT refresh_token, email FROM oauth_microsoft_pending WHERE state=? AND refresh_token IS NOT NULL AND email IS NOT NULL`,
|
||||
state,
|
||||
)
|
||||
if err := row.Scan(&refresh, &email); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return "", "", ErrOAuthPendingNotFound
|
||||
}
|
||||
return "", "", err
|
||||
}
|
||||
_, _ = database.DB.Exec(`DELETE FROM oauth_microsoft_pending WHERE state=?`, state)
|
||||
return refresh, email, nil
|
||||
}
|
||||
@@ -1,50 +1,50 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"mengpost-backend/database"
|
||||
)
|
||||
|
||||
// SentLog 记录一次外发邮件的结果.
|
||||
type SentLog struct {
|
||||
ID int64 `json:"id"`
|
||||
AccountID int64 `json:"account_id"`
|
||||
To string `json:"to"`
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func AddSentLog(l *SentLog) error {
|
||||
_, err := database.DB.Exec(
|
||||
`INSERT INTO sent_logs(account_id,to_addr,subject,body,status,error) VALUES(?,?,?,?,?,?)`,
|
||||
l.AccountID, l.To, l.Subject, l.Body, l.Status, l.Error,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func ListSentLogs(accountID int64, limit int) ([]SentLog, error) {
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
rows, err := database.DB.Query(
|
||||
`SELECT id,account_id,to_addr,subject,body,status,error,created_at FROM sent_logs WHERE account_id=? ORDER BY id DESC LIMIT ?`,
|
||||
accountID, limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []SentLog
|
||||
for rows.Next() {
|
||||
var l SentLog
|
||||
if err := rows.Scan(&l.ID, &l.AccountID, &l.To, &l.Subject, &l.Body, &l.Status, &l.Error, &l.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, l)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"mengpost-backend/database"
|
||||
)
|
||||
|
||||
// SentLog 记录一次外发邮件的结果.
|
||||
type SentLog struct {
|
||||
ID int64 `json:"id"`
|
||||
AccountID int64 `json:"account_id"`
|
||||
To string `json:"to"`
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func AddSentLog(l *SentLog) error {
|
||||
_, err := database.DB.Exec(
|
||||
`INSERT INTO sent_logs(account_id,to_addr,subject,body,status,error) VALUES(?,?,?,?,?,?)`,
|
||||
l.AccountID, l.To, l.Subject, l.Body, l.Status, l.Error,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func ListSentLogs(accountID int64, limit int) ([]SentLog, error) {
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
rows, err := database.DB.Query(
|
||||
`SELECT id,account_id,to_addr,subject,body,status,error,created_at FROM sent_logs WHERE account_id=? ORDER BY id DESC LIMIT ?`,
|
||||
accountID, limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []SentLog
|
||||
for rows.Next() {
|
||||
var l SentLog
|
||||
if err := rows.Scan(&l.ID, &l.AccountID, &l.To, &l.Subject, &l.Body, &l.Status, &l.Error, &l.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, l)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user