|
@@ -4,12 +4,59 @@ import (
|
|
"database/sql"
|
|
"database/sql"
|
|
"encoding/binary"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"encoding/json"
|
|
|
|
+ "errors"
|
|
|
|
+ "fmt"
|
|
"github.com/flashmob/go-guerrilla/backends"
|
|
"github.com/flashmob/go-guerrilla/backends"
|
|
"github.com/flashmob/go-guerrilla/mail"
|
|
"github.com/flashmob/go-guerrilla/mail"
|
|
"github.com/flashmob/go-guerrilla/mail/smtp"
|
|
"github.com/flashmob/go-guerrilla/mail/smtp"
|
|
|
|
+ "github.com/go-sql-driver/mysql"
|
|
"net"
|
|
"net"
|
|
|
|
+ "time"
|
|
)
|
|
)
|
|
|
|
|
|
|
|
+/*
|
|
|
|
+
|
|
|
|
+SQL schema
|
|
|
|
+
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+create schema gmail collate utf8mb4_unicode_ci;
|
|
|
|
+
|
|
|
|
+CREATE TABLE `in_emails` (
|
|
|
|
+ `mail_id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
|
|
|
+ `created_at` datetime NOT NULL,
|
|
|
|
+ `size` int unsigned NOT NULL,
|
|
|
|
+ `from` varbinary(255) NOT NULL,
|
|
|
|
+ `to` varbinary(255) NOT NULL,
|
|
|
|
+ `parts_info` text COLLATE utf8mb4_unicode_ci,
|
|
|
|
+ `helo` varchar(255) COLLATE latin1_swedish_ci NOT NULL,
|
|
|
|
+ `subject` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
|
|
|
+ `queued_id` binary(16) NOT NULL,
|
|
|
|
+ `recipient` varbinary(255) NOT NULL,
|
|
|
|
+ `ipv4_addr` int unsigned DEFAULT NULL,
|
|
|
|
+ `ipv6_addr` varbinary(16) DEFAULT NULL,
|
|
|
|
+ `return_path` varbinary(255) NOT NULL,
|
|
|
|
+ `protocol` set('SMTP','SMTPS','ESMTP','ESMTPS','LMTP','LMTPS') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'SMTP',
|
|
|
|
+ `transport` set('7bit','8bit','unknown','invalid') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'unknown',
|
|
|
|
+ PRIMARY KEY (`mail_id`)
|
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
+
|
|
|
|
+CREATE TABLE `in_emails_chunks` (
|
|
|
|
+ `modified_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
+ `reference_count` int unsigned DEFAULT '1',
|
|
|
|
+ `data` mediumblob NOT NULL,
|
|
|
|
+ `hash` varbinary(16) NOT NULL,
|
|
|
|
+ UNIQUE KEY `in_emails_chunks_hash_uindex` (`hash`)
|
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+ipv6_addr is big endian
|
|
|
|
+
|
|
|
|
+TODO compression, configurable SQL strings, logger
|
|
|
|
+
|
|
|
|
+*/
|
|
func init() {
|
|
func init() {
|
|
StorageEngines["sql"] = func() Storage {
|
|
StorageEngines["sql"] = func() Storage {
|
|
return new(StoreSQL)
|
|
return new(StoreSQL)
|
|
@@ -17,12 +64,28 @@ func init() {
|
|
}
|
|
}
|
|
|
|
|
|
type sqlConfig struct {
|
|
type sqlConfig struct {
|
|
- EmailTable string `json:"email_table,omitempty"`
|
|
|
|
- ChunkTable string `json:"chunk_table,omitempty"`
|
|
|
|
- Driver string `json:"sql_driver,omitempty"`
|
|
|
|
- DSN string `json:"sql_dsn,omitempty"`
|
|
|
|
- PrimaryHost string `json:"primary_mail_host,omitempty"`
|
|
|
|
- CompressLevel int `json:"compress_level,omitempty"`
|
|
|
|
|
|
+
|
|
|
|
+ // EmailTable is the name of the main database table for the headers
|
|
|
|
+ EmailTable string `json:"email_table,omitempty"`
|
|
|
|
+ // EmailChunkTable stores the data of the emails in de-duplicated chunks
|
|
|
|
+ EmailChunkTable string `json:"email_table_chunks,omitempty"`
|
|
|
|
+
|
|
|
|
+ // Connection settings
|
|
|
|
+ // Driver to use, eg "mysql"
|
|
|
|
+ Driver string `json:"sql_driver,omitempty"`
|
|
|
|
+ // DSN (required) is the connection string, eg.
|
|
|
|
+ // "user:passt@tcp(127.0.0.1:3306)/db_name?readTimeout=10s&writeTimeout=10s&charset=utf8mb4&collation=utf8mb4_unicode_ci"
|
|
|
|
+ DSN string `json:"sql_dsn,omitempty"`
|
|
|
|
+ // MaxConnLifetime (optional) is a duration, eg. "30s"
|
|
|
|
+ MaxConnLifetime string `json:"sql_max_conn_lifetime,omitempty"`
|
|
|
|
+ // MaxOpenConns (optional) specifies the number of maximum open connections
|
|
|
|
+ MaxOpenConns int `json:"sql_max_open_conns,omitempty"`
|
|
|
|
+ // MaxIdleConns
|
|
|
|
+ MaxIdleConns int `json:"sql_max_idle_conns,omitempty"`
|
|
|
|
+
|
|
|
|
+ // CompressLevel controls the gzip compression level of email chunks.
|
|
|
|
+ // 0 = no compression, 1 == best speed, 9 == best compression, -1 == default, -2 == huffman only
|
|
|
|
+ CompressLevel int `json:"compress_level,omitempty"`
|
|
}
|
|
}
|
|
|
|
|
|
// StoreSQL implements the Storage interface
|
|
// StoreSQL implements the Storage interface
|
|
@@ -32,12 +95,51 @@ type StoreSQL struct {
|
|
db *sql.DB
|
|
db *sql.DB
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+func (s *StoreSQL) StartWorker() (stop chan bool) {
|
|
|
|
+
|
|
|
|
+ timeo := time.Second * 1
|
|
|
|
+ stop = make(chan bool)
|
|
|
|
+ go func() {
|
|
|
|
+ select {
|
|
|
|
+
|
|
|
|
+ case <-stop:
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ case <-time.After(timeo):
|
|
|
|
+ t1 := int64(time.Now().UnixNano())
|
|
|
|
+ // do stuff here
|
|
|
|
+
|
|
|
|
+ if (time.Now().UnixNano())-t1 > int64(time.Second*3) {
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ }()
|
|
|
|
+ return stop
|
|
|
|
+
|
|
|
|
+}
|
|
|
|
+
|
|
func (s *StoreSQL) connect() (*sql.DB, error) {
|
|
func (s *StoreSQL) connect() (*sql.DB, error) {
|
|
var err error
|
|
var err error
|
|
if s.db, err = sql.Open(s.config.Driver, s.config.DSN); err != nil {
|
|
if s.db, err = sql.Open(s.config.Driver, s.config.DSN); err != nil {
|
|
backends.Log().Error("cannot open database: ", err)
|
|
backends.Log().Error("cannot open database: ", err)
|
|
return nil, err
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
+ if s.config.MaxOpenConns != 0 {
|
|
|
|
+ s.db.SetMaxOpenConns(s.config.MaxOpenConns)
|
|
|
|
+ }
|
|
|
|
+ if s.config.MaxIdleConns != 0 {
|
|
|
|
+ s.db.SetMaxIdleConns(s.config.MaxIdleConns)
|
|
|
|
+ }
|
|
|
|
+ if s.config.MaxConnLifetime != "" {
|
|
|
|
+ t, err := time.ParseDuration(s.config.MaxConnLifetime)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return nil, err
|
|
|
|
+ }
|
|
|
|
+ s.db.SetConnMaxLifetime(t)
|
|
|
|
+ }
|
|
|
|
+ stats := s.db.Stats()
|
|
|
|
+ fmt.Println(stats)
|
|
// do we have permission to access the table?
|
|
// do we have permission to access the table?
|
|
_, err = s.db.Query("SELECT mail_id FROM " + s.config.EmailTable + " LIMIT 1")
|
|
_, err = s.db.Query("SELECT mail_id FROM " + s.config.EmailTable + " LIMIT 1")
|
|
if err != nil {
|
|
if err != nil {
|
|
@@ -54,8 +156,8 @@ func (s *StoreSQL) prepareSql() error {
|
|
// begin inserting an email (before saving chunks)
|
|
// begin inserting an email (before saving chunks)
|
|
if stmt, err := s.db.Prepare(`INSERT INTO ` +
|
|
if stmt, err := s.db.Prepare(`INSERT INTO ` +
|
|
s.config.EmailTable +
|
|
s.config.EmailTable +
|
|
- ` (from, helo, recipient, ipv4_addr, ipv6_addr, return_path, transport, protocol)
|
|
|
|
- VALUES(?, ?, ?, ?, ?, ?, ?, ?)`); err != nil {
|
|
|
|
|
|
+ ` (queued_id, created_at, ` + "`from`" + `, helo, recipient, ipv4_addr, ipv6_addr, return_path, transport, protocol)
|
|
|
|
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`); err != nil {
|
|
return err
|
|
return err
|
|
} else {
|
|
} else {
|
|
s.statements["insertEmail"] = stmt
|
|
s.statements["insertEmail"] = stmt
|
|
@@ -63,7 +165,7 @@ func (s *StoreSQL) prepareSql() error {
|
|
|
|
|
|
// insert a chunk of email's data
|
|
// insert a chunk of email's data
|
|
if stmt, err := s.db.Prepare(`INSERT INTO ` +
|
|
if stmt, err := s.db.Prepare(`INSERT INTO ` +
|
|
- s.config.ChunkTable +
|
|
|
|
|
|
+ s.config.EmailChunkTable +
|
|
` (data, hash)
|
|
` (data, hash)
|
|
VALUES(?, ?)`); err != nil {
|
|
VALUES(?, ?)`); err != nil {
|
|
return err
|
|
return err
|
|
@@ -74,7 +176,7 @@ func (s *StoreSQL) prepareSql() error {
|
|
// finalize the email (the connection closed)
|
|
// finalize the email (the connection closed)
|
|
if stmt, err := s.db.Prepare(`
|
|
if stmt, err := s.db.Prepare(`
|
|
UPDATE ` + s.config.EmailTable + `
|
|
UPDATE ` + s.config.EmailTable + `
|
|
- SET size=?, parts_info = ?, subject, queued_id = ?, to = ?
|
|
|
|
|
|
+ SET size=?, parts_info=?, subject=?, ` + "`to`" + `=?, ` + "`from`" + `=?
|
|
WHERE mail_id = ? `); err != nil {
|
|
WHERE mail_id = ? `); err != nil {
|
|
return err
|
|
return err
|
|
} else {
|
|
} else {
|
|
@@ -85,7 +187,7 @@ func (s *StoreSQL) prepareSql() error {
|
|
// This means we can avoid re-inserting an existing chunk, only update its reference_count
|
|
// This means we can avoid re-inserting an existing chunk, only update its reference_count
|
|
// check the "affected rows" count after executing query
|
|
// check the "affected rows" count after executing query
|
|
if stmt, err := s.db.Prepare(`
|
|
if stmt, err := s.db.Prepare(`
|
|
- UPDATE ` + s.config.ChunkTable + `
|
|
|
|
|
|
+ UPDATE ` + s.config.EmailChunkTable + `
|
|
SET reference_count=reference_count+1
|
|
SET reference_count=reference_count+1
|
|
WHERE hash = ? `); err != nil {
|
|
WHERE hash = ? `); err != nil {
|
|
return err
|
|
return err
|
|
@@ -96,7 +198,7 @@ func (s *StoreSQL) prepareSql() error {
|
|
// If the reference_count is 0 then it means the chunk has been deleted
|
|
// If the reference_count is 0 then it means the chunk has been deleted
|
|
// Chunks are soft-deleted for now, hard-deleted by another sweeper query as they become stale.
|
|
// Chunks are soft-deleted for now, hard-deleted by another sweeper query as they become stale.
|
|
if stmt, err := s.db.Prepare(`
|
|
if stmt, err := s.db.Prepare(`
|
|
- UPDATE ` + s.config.ChunkTable + `
|
|
|
|
|
|
+ UPDATE ` + s.config.EmailChunkTable + `
|
|
SET reference_count=reference_count-1
|
|
SET reference_count=reference_count-1
|
|
WHERE hash = ? AND reference_count > 0`); err != nil {
|
|
WHERE hash = ? AND reference_count > 0`); err != nil {
|
|
return err
|
|
return err
|
|
@@ -117,7 +219,7 @@ func (s *StoreSQL) prepareSql() error {
|
|
// fetch a chunk
|
|
// fetch a chunk
|
|
if stmt, err := s.db.Prepare(`
|
|
if stmt, err := s.db.Prepare(`
|
|
SELECT *
|
|
SELECT *
|
|
- from ` + s.config.ChunkTable + `
|
|
|
|
|
|
+ from ` + s.config.EmailChunkTable + `
|
|
where hash=?`); err != nil {
|
|
where hash=?`); err != nil {
|
|
return err
|
|
return err
|
|
} else {
|
|
} else {
|
|
@@ -131,12 +233,15 @@ func (s *StoreSQL) prepareSql() error {
|
|
return nil
|
|
return nil
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+const mysqlYYYY_m_d_s_H_i_s = "2006-01-02 15:04:05"
|
|
|
|
+
|
|
// OpenMessage implements the Storage interface
|
|
// OpenMessage implements the Storage interface
|
|
func (s *StoreSQL) OpenMessage(
|
|
func (s *StoreSQL) OpenMessage(
|
|
|
|
+ queuedID mail.Hash128,
|
|
from string,
|
|
from string,
|
|
helo string,
|
|
helo string,
|
|
recipient string,
|
|
recipient string,
|
|
- ipAddress net.IPAddr,
|
|
|
|
|
|
+ ipAddress IPAddr,
|
|
returnPath string,
|
|
returnPath string,
|
|
protocol mail.Protocol,
|
|
protocol mail.Protocol,
|
|
transport smtp.TransportType,
|
|
transport smtp.TransportType,
|
|
@@ -148,9 +253,19 @@ func (s *StoreSQL) OpenMessage(
|
|
if ip := ipAddress.IP.To4(); ip != nil {
|
|
if ip := ipAddress.IP.To4(); ip != nil {
|
|
ip4 = binary.BigEndian.Uint32(ip)
|
|
ip4 = binary.BigEndian.Uint32(ip)
|
|
} else {
|
|
} else {
|
|
- _ = copy(ip6, ipAddress.IP)
|
|
|
|
|
|
+ copy(ip6, ipAddress.IP)
|
|
}
|
|
}
|
|
- r, err := s.statements["insertEmail"].Exec(from, helo, recipient, ip4, ip6, returnPath, transport, protocol)
|
|
|
|
|
|
+ r, err := s.statements["insertEmail"].Exec(
|
|
|
|
+ queuedID.Bytes(),
|
|
|
|
+ time.Now().Format(mysqlYYYY_m_d_s_H_i_s),
|
|
|
|
+ from,
|
|
|
|
+ helo,
|
|
|
|
+ recipient,
|
|
|
|
+ ip4,
|
|
|
|
+ ip6,
|
|
|
|
+ returnPath,
|
|
|
|
+ transport.String(),
|
|
|
|
+ protocol.String())
|
|
if err != nil {
|
|
if err != nil {
|
|
return 0, err
|
|
return 0, err
|
|
}
|
|
}
|
|
@@ -188,13 +303,12 @@ func (s *StoreSQL) CloseMessage(
|
|
size int64,
|
|
size int64,
|
|
partsInfo *PartsInfo,
|
|
partsInfo *PartsInfo,
|
|
subject string,
|
|
subject string,
|
|
- queuedID string,
|
|
|
|
to string, from string) error {
|
|
to string, from string) error {
|
|
partsInfoJson, err := json.Marshal(partsInfo)
|
|
partsInfoJson, err := json.Marshal(partsInfo)
|
|
if err != nil {
|
|
if err != nil {
|
|
return err
|
|
return err
|
|
}
|
|
}
|
|
- _, err = s.statements["finalizeEmail"].Exec(size, partsInfoJson, subject, queuedID, to, mailID)
|
|
|
|
|
|
+ _, err = s.statements["finalizeEmail"].Exec(size, partsInfoJson, subject, to, from, mailID)
|
|
if err != nil {
|
|
if err != nil {
|
|
return err
|
|
return err
|
|
}
|
|
}
|
|
@@ -208,6 +322,16 @@ func (s *StoreSQL) Initialize(cfg backends.ConfigGroup) error {
|
|
if err != nil {
|
|
if err != nil {
|
|
return err
|
|
return err
|
|
}
|
|
}
|
|
|
|
+ if s.config.EmailTable == "" {
|
|
|
|
+ s.config.EmailTable = "in_emails"
|
|
|
|
+ }
|
|
|
|
+ if s.config.EmailChunkTable == "" {
|
|
|
|
+ s.config.EmailChunkTable = "in_emails_chunks"
|
|
|
|
+ }
|
|
|
|
+ if s.config.Driver == "" {
|
|
|
|
+ s.config.Driver = "mysql"
|
|
|
|
+ }
|
|
|
|
+
|
|
s.db, err = s.connect()
|
|
s.db, err = s.connect()
|
|
if err != nil {
|
|
if err != nil {
|
|
return err
|
|
return err
|
|
@@ -238,11 +362,126 @@ func (s *StoreSQL) Shutdown() (err error) {
|
|
|
|
|
|
// GetEmail implements the Storage interface
|
|
// GetEmail implements the Storage interface
|
|
func (s *StoreSQL) GetEmail(mailID uint64) (*Email, error) {
|
|
func (s *StoreSQL) GetEmail(mailID uint64) (*Email, error) {
|
|
- return &Email{}, nil
|
|
|
|
|
|
+
|
|
|
|
+ email := &Email{}
|
|
|
|
+ var createdAt mysql.NullTime
|
|
|
|
+ var transport transportType
|
|
|
|
+ var protocol protocol
|
|
|
|
+ err := s.statements["selectMail"].QueryRow(mailID).Scan(
|
|
|
|
+ &email.mailID,
|
|
|
|
+ &createdAt,
|
|
|
|
+ &email.size,
|
|
|
|
+ &email.from,
|
|
|
|
+ &email.to,
|
|
|
|
+ &email.partsInfo,
|
|
|
|
+ &email.helo,
|
|
|
|
+ &email.subject,
|
|
|
|
+ &email.queuedID,
|
|
|
|
+ &email.recipient,
|
|
|
|
+ &email.ipv4,
|
|
|
|
+ &email.ipv6,
|
|
|
|
+ &email.returnPath,
|
|
|
|
+ &protocol,
|
|
|
|
+ &transport,
|
|
|
|
+ )
|
|
|
|
+ email.createdAt = createdAt.Time
|
|
|
|
+ email.protocol = protocol.Protocol
|
|
|
|
+ email.transport = transport.TransportType
|
|
|
|
+ if err != nil {
|
|
|
|
+ return email, err
|
|
|
|
+ }
|
|
|
|
+ return email, nil
|
|
}
|
|
}
|
|
|
|
|
|
-// GetChunk implements the Storage interface
|
|
|
|
|
|
+// GetChunks implements the Storage interface
|
|
func (s *StoreSQL) GetChunks(hash ...HashKey) ([]*Chunk, error) {
|
|
func (s *StoreSQL) GetChunks(hash ...HashKey) ([]*Chunk, error) {
|
|
result := make([]*Chunk, 0, len(hash))
|
|
result := make([]*Chunk, 0, len(hash))
|
|
return result, nil
|
|
return result, nil
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+// zap is used in testing, purges everything
|
|
|
|
+func (s *StoreSQL) zap() error {
|
|
|
|
+ if r, err := s.db.Exec("DELETE from " + s.config.EmailTable + " "); err != nil {
|
|
|
|
+ return err
|
|
|
|
+ } else {
|
|
|
|
+ affected, _ := r.RowsAffected()
|
|
|
|
+ fmt.Println(fmt.Sprintf("deleted %v emails", affected))
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if r, err := s.db.Exec("DELETE from " + s.config.EmailChunkTable + " "); err != nil {
|
|
|
|
+ return err
|
|
|
|
+ } else {
|
|
|
|
+ affected, _ := r.RowsAffected()
|
|
|
|
+ fmt.Println(fmt.Sprintf("deleted %v chunks", affected))
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return nil
|
|
|
|
+
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Scan implements database/sql scanner interface, for parsing PartsInfo
|
|
|
|
+func (info *PartsInfo) Scan(value interface{}) error {
|
|
|
|
+ if value == nil {
|
|
|
|
+ return errors.New("parts_info is null")
|
|
|
|
+ }
|
|
|
|
+ if data, ok := value.([]byte); !ok {
|
|
|
|
+ return errors.New("parts_info is not str")
|
|
|
|
+ } else {
|
|
|
|
+ if err := json.Unmarshal(data, info); err != nil {
|
|
|
|
+ return err
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return nil
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// /Scan implements database/sql scanner interface, for parsing net.IPAddr
|
|
|
|
+func (ip *IPAddr) Scan(value interface{}) error {
|
|
|
|
+ if value == nil {
|
|
|
|
+ ip = nil
|
|
|
|
+ return nil
|
|
|
|
+ }
|
|
|
|
+ if data, ok := value.([]uint8); ok {
|
|
|
|
+ if len(data) == 16 { // 128 bits
|
|
|
|
+ // ipv6
|
|
|
|
+ ipv6 := make(net.IP, 16)
|
|
|
|
+ copy(ipv6, data)
|
|
|
|
+ ip.IPAddr.IP = ipv6
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if data, ok := value.(int64); ok {
|
|
|
|
+ // ipv4
|
|
|
|
+ ipv4 := make(net.IP, 4)
|
|
|
|
+ binary.BigEndian.PutUint32(ipv4, uint32(data))
|
|
|
|
+ ip.IPAddr.IP = ipv4
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return nil
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+type transportType struct {
|
|
|
|
+ smtp.TransportType
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+type protocol struct {
|
|
|
|
+ mail.Protocol
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// todo scanners for protocol & transport
|
|
|
|
+
|
|
|
|
+// Scan implements database/sql scanner interface, for parsing smtp.TransportType
|
|
|
|
+func (t *transportType) Scan(value interface{}) error {
|
|
|
|
+ if data, ok := value.([]uint8); ok {
|
|
|
|
+ v := smtp.ParseTransportType(string(data))
|
|
|
|
+ t.TransportType = v
|
|
|
|
+ }
|
|
|
|
+ return nil
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Scan implements database/sql scanner interface, for parsing mail.Protocol
|
|
|
|
+func (p *protocol) Scan(value interface{}) error {
|
|
|
|
+ if data, ok := value.([]uint8); ok {
|
|
|
|
+ v := mail.ParseProtocolType(string(data))
|
|
|
|
+ p.Protocol = v
|
|
|
|
+ }
|
|
|
|
+ return nil
|
|
|
|
+}
|