master
兔子 1 month ago
parent 35cf23e5ad
commit 231cad54d0

@ -0,0 +1,52 @@
package cert
import (
"b612.me/starcrypto"
"crypto"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"errors"
"os"
)
func MakeCert(caKey any, caCrt *x509.Certificate, csr *x509.Certificate, pub any) ([]byte, error) {
der, err := x509.CreateCertificate(rand.Reader, csr, caCrt, pub, caKey)
if err != nil {
return nil, err
}
cert, err := x509.ParseCertificate(der)
if err != nil {
return nil, err
}
certBlock := &pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
}
pemData := pem.EncodeToMemory(certBlock)
return pemData, nil
}
func LoadCA(caKeyPath, caCertPath, KeyPwd string) (crypto.PrivateKey, *x509.Certificate, error) {
caKeyBytes, err := os.ReadFile(caKeyPath)
if err != nil {
return nil, nil, err
}
caCertBytes, err := os.ReadFile(caCertPath)
if err != nil {
return nil, nil, err
}
caKey, err := starcrypto.DecodePrivateKey(caKeyBytes, KeyPwd)
if err != nil {
return nil, nil, err
}
block, _ := pem.Decode(caCertBytes)
if block == nil || (block.Type != "CERTIFICATE" && block.Type != "CERTIFICATE REQUEST") {
return nil, nil, errors.New("Failed to decode PEM block containing the certificate")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, nil, err
}
return caKey, cert, nil
}

@ -0,0 +1,637 @@
package cert
import (
"b612.me/starlog"
"crypto/dsa"
"crypto/ecdh"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"os"
"software.sslmate.com/src/go-pkcs12"
"strings"
)
func ParseCert(data []byte, pwd string) {
{
pems, err := pkcs12.ToPEM(data, pwd)
if err == nil {
for _, v := range pems {
switch v.Type {
case "CERTIFICATE":
cert, err := x509.ParseCertificate(v.Bytes)
if err != nil {
continue
}
starlog.Green("这是一个PKCS12文件\n")
starlog.Green("-----证书信息-----\n\n")
starlog.Green("证书版本:%d\n", cert.Version)
starlog.Green("证书序列号:%d\n", cert.SerialNumber)
starlog.Green("证书签发者:%s\n", cert.Issuer)
starlog.Green("证书开始时间:%s\n", cert.NotBefore)
starlog.Green("证书结束时间:%s\n", cert.NotAfter)
starlog.Green("证书扩展:%+v\n", cert.Extensions)
starlog.Green("证书签名算法:%s\n", cert.SignatureAlgorithm)
starlog.Green("证书签名:%x\n", cert.Signature)
starlog.Green("证书公钥:%+v\n", cert.PublicKey)
starlog.Green("证书公钥算法:%s\n", cert.PublicKeyAlgorithm)
starlog.Green("证书密钥用法:%v\n", keyUsageToStrings(cert.KeyUsage))
starlog.Green("证书扩展密钥用法:%v\n", extKeyUsageToStrings(cert.ExtKeyUsage))
starlog.Green("证书是否CA%t\n", cert.IsCA)
starlog.Green("证书最大路径长度:%d\n", cert.MaxPathLen)
starlog.Green("证书最大路径长度是否为0%t\n", cert.MaxPathLenZero)
starlog.Green("证书是否根证书:%t\n", cert.BasicConstraintsValid)
starlog.Green("证书国家:%+v\n", cert.Subject.Country)
starlog.Green("证书省份:%+v\n", cert.Subject.Province)
starlog.Green("证书城市:%+v\n", cert.Subject.Locality)
starlog.Green("证书组织:%+v\n", cert.Subject.Organization)
starlog.Green("证书组织单位:%+v\n", cert.Subject.OrganizationalUnit)
starlog.Green("证书通用名称:%s\n", cert.Subject.CommonName)
starlog.Green("证书DNS%+v\n", cert.DNSNames)
starlog.Green("证书主题:%s\n", cert.Subject.String())
starlog.Green("-----公钥信息-----\n\n")
pub := cert.PublicKey
switch n := pub.(type) {
case *rsa.PublicKey:
starlog.Green("公钥算法为RSA\n")
starlog.Green("公钥位数:%d\n", n.Size())
starlog.Green("公钥长度:%d\n", n.N.BitLen())
starlog.Green("公钥指数:%d\n", n.E)
case *ecdsa.PublicKey:
starlog.Green("公钥算法为ECDSA\n")
starlog.Green("公钥位数:%d\n", n.Curve.Params().BitSize)
starlog.Green("公钥曲线:%s\n", n.Curve.Params().Name)
starlog.Green("公钥长度:%d\n", n.Params().BitSize)
starlog.Green("公钥公钥X%d\n", n.X)
starlog.Green("公钥公钥Y%d\n", n.Y)
case *dsa.PublicKey:
starlog.Green("公钥算法为DSA\n")
starlog.Green("公钥公钥Y%d\n", n.Y)
case *ecdh.PublicKey:
starlog.Green("公钥算法为ECDH\n")
case *ed25519.PublicKey:
starlog.Green("公钥算法为ED25519\n")
default:
starlog.Green("未知公钥类型\n")
}
case "PRIVATE KEY":
priv, err := x509.ParsePKCS8PrivateKey(v.Bytes)
if err != nil {
priv, err = x509.ParsePKCS1PrivateKey(v.Bytes)
if err != nil {
priv, err = x509.ParseECPrivateKey(v.Bytes)
if err != nil {
starlog.Errorf("解析私钥错误:%s\n", err)
continue
} else {
starlog.Green("这是一个ECDSA私钥\n")
}
} else {
starlog.Green("这是一个PKCS1私钥\n")
}
} else {
starlog.Green("这是一个PKCS8私钥\n")
}
starlog.Green("-----私钥信息-----\n\n")
switch n := priv.(type) {
case *rsa.PrivateKey:
starlog.Green("这是一个RSA私钥\n")
starlog.Green("私钥位数:%d\n", n.Size())
starlog.Green("私钥长度:%d\n", n.N.BitLen())
starlog.Green("私钥指数:%d\n", n.E)
starlog.Green("私钥系数:%d\n", n.D)
starlog.Green("私钥质数p%d\n", n.Primes[0])
starlog.Green("私钥质数q%d\n", n.Primes[1])
starlog.Green("私钥系数dP%d\n", n.Precomputed.Dp)
starlog.Green("私钥系数dQ%d\n", n.Precomputed.Dq)
starlog.Green("私钥系数qInv%d\n", n.Precomputed.Qinv)
case *ecdsa.PrivateKey:
starlog.Green("这是一个ECDSA私钥\n")
starlog.Green("私钥位数:%d\n", n.Curve.Params().BitSize)
starlog.Green("私钥曲线:%s\n", n.Curve.Params().Name)
starlog.Green("私钥长度:%d\n", n.Params().BitSize)
starlog.Green("私钥系数:%d\n", n.D)
starlog.Green("私钥公钥X%d\n", n.PublicKey.X)
starlog.Green("私钥公钥Y%d\n", n.PublicKey.Y)
case *dsa.PrivateKey:
starlog.Green("这是一个DSA私钥\n")
starlog.Green("私钥系数:%d\n", n.X)
starlog.Green("私钥公钥Y%d\n", n.Y)
case *ed25519.PrivateKey:
starlog.Green("这是一个ED25519私钥\n")
case *ecdh.PrivateKey:
starlog.Green("这是一个ECDH私钥\n")
default:
starlog.Green("未知私钥类型\n")
}
}
}
return
}
}
idx := 0
for {
idx++
block, rest := pem.Decode(data)
if block == nil {
if idx == 1 {
starlog.Errorf("未知文件类型\n")
}
return
}
fmt.Println("\n--------------------------------\n")
data = rest
rt := 0
switch block.Type {
case "CERTIFICATE":
rt = 1
starlog.Green("这是一个证书文件\n")
fallthrough
case "CERTIFICATE REQUEST":
if rt == 0 {
starlog.Green("这是一个证书请求文件\n")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
starlog.Errorf("解析证书错误:%s\n", err)
continue
}
starlog.Green("证书版本:%d\n", cert.Version)
starlog.Green("证书序列号:%d\n", cert.SerialNumber)
starlog.Green("证书签发者:%s\n", cert.Issuer)
starlog.Green("证书开始时间:%s\n", cert.NotBefore)
starlog.Green("证书结束时间:%s\n", cert.NotAfter)
starlog.Green("证书扩展:%+v\n", cert.Extensions)
starlog.Green("证书签名算法:%s\n", cert.SignatureAlgorithm)
starlog.Green("证书签名:%x\n", cert.Signature)
starlog.Green("证书公钥:%+v\n", cert.PublicKey)
starlog.Green("证书公钥算法:%s\n", cert.PublicKeyAlgorithm)
starlog.Green("证书密钥用法:%v\n", keyUsageToStrings(cert.KeyUsage))
starlog.Green("证书扩展密钥用法:%v\n", extKeyUsageToStrings(cert.ExtKeyUsage))
starlog.Green("证书是否CA%t\n", cert.IsCA)
starlog.Green("证书最大路径长度:%d\n", cert.MaxPathLen)
starlog.Green("证书最大路径长度是否为0%t\n", cert.MaxPathLenZero)
starlog.Green("证书是否根证书:%t\n", cert.BasicConstraintsValid)
starlog.Green("证书国家:%+v\n", cert.Subject.Country)
starlog.Green("证书省份:%+v\n", cert.Subject.Province)
starlog.Green("证书城市:%+v\n", cert.Subject.Locality)
starlog.Green("证书组织:%+v\n", cert.Subject.Organization)
starlog.Green("证书组织单位:%+v\n", cert.Subject.OrganizationalUnit)
starlog.Green("证书通用名称:%s\n", cert.Subject.CommonName)
starlog.Green("证书DNS%+v\n", cert.DNSNames)
starlog.Green("证书主题:%s\n", cert.Subject.String())
pub := cert.PublicKey
switch n := pub.(type) {
case *rsa.PublicKey:
starlog.Green("公钥算法为RSA\n")
starlog.Green("公钥位数:%d\n", n.Size())
starlog.Green("公钥长度:%d\n", n.N.BitLen())
starlog.Green("公钥指数:%d\n", n.E)
case *ecdsa.PublicKey:
starlog.Green("公钥算法为ECDSA\n")
starlog.Green("公钥位数:%d\n", n.Curve.Params().BitSize)
starlog.Green("公钥曲线:%s\n", n.Curve.Params().Name)
starlog.Green("公钥长度:%d\n", n.Params().BitSize)
starlog.Green("公钥公钥X%d\n", n.X)
starlog.Green("公钥公钥Y%d\n", n.Y)
case *dsa.PublicKey:
starlog.Green("公钥算法为DSA\n")
starlog.Green("公钥公钥Y%d\n", n.Y)
case *ecdh.PublicKey:
starlog.Green("公钥算法为ECDH\n")
case *ed25519.PublicKey:
starlog.Green("公钥算法为ED25519\n")
default:
starlog.Green("未知公钥类型\n")
}
continue
case "PRIVATE KEY":
starlog.Infof("这是一个私钥文件\n")
priv, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
priv, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
priv, err = x509.ParseECPrivateKey(block.Bytes)
if err != nil {
starlog.Errorf("解析私钥错误:%s\n", err)
continue
} else {
starlog.Green("这是一个ECDSA私钥\n")
}
} else {
starlog.Green("这是一个PKCS1私钥\n")
}
} else {
starlog.Green("这是一个PKCS8私钥\n")
}
switch n := priv.(type) {
case *rsa.PrivateKey:
starlog.Green("这是一个RSA私钥\n")
starlog.Green("私钥位数:%d\n", n.Size())
starlog.Green("私钥长度:%d\n", n.N.BitLen())
starlog.Green("私钥指数:%d\n", n.E)
starlog.Green("私钥系数:%d\n", n.D)
starlog.Green("私钥质数p%d\n", n.Primes[0])
starlog.Green("私钥质数q%d\n", n.Primes[1])
starlog.Green("私钥系数dP%d\n", n.Precomputed.Dp)
starlog.Green("私钥系数dQ%d\n", n.Precomputed.Dq)
starlog.Green("私钥系数qInv%d\n", n.Precomputed.Qinv)
case *ecdsa.PrivateKey:
starlog.Green("这是一个ECDSA私钥\n")
starlog.Green("私钥位数:%d\n", n.Curve.Params().BitSize)
starlog.Green("私钥曲线:%s\n", n.Curve.Params().Name)
starlog.Green("私钥长度:%d\n", n.Params().BitSize)
starlog.Green("私钥系数:%d\n", n.D)
starlog.Green("私钥公钥X%d\n", n.PublicKey.X)
starlog.Green("私钥公钥Y%d\n", n.PublicKey.Y)
case *dsa.PrivateKey:
starlog.Green("这是一个DSA私钥\n")
starlog.Green("私钥系数:%d\n", n.X)
starlog.Green("私钥公钥Y%d\n", n.Y)
case *ed25519.PrivateKey:
starlog.Green("这是一个ED25519私钥\n")
case *ecdh.PrivateKey:
starlog.Green("这是一个ECDH私钥\n")
default:
starlog.Green("未知私钥类型\n")
}
continue
case "PUBLIC KEY":
starlog.Infof("这是一个公钥文件\n")
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
pub, err = x509.ParsePKCS1PublicKey(block.Bytes)
starlog.Green("这是一个PKCS1公钥\n")
} else {
starlog.Green("这是一个PKIX公钥\n")
}
switch n := pub.(type) {
case *rsa.PublicKey:
starlog.Green("这是一个RSA公钥\n")
starlog.Green("公钥位数:%d\n", n.Size())
starlog.Green("公钥长度:%d\n", n.N.BitLen())
starlog.Green("公钥指数:%d\n", n.E)
case *ecdsa.PublicKey:
starlog.Green("这是一个ECDSA公钥\n")
starlog.Green("公钥位数:%d\n", n.Curve.Params().BitSize)
starlog.Green("公钥曲线:%s\n", n.Curve.Params().Name)
starlog.Green("公钥长度:%d\n", n.Params().BitSize)
starlog.Green("公钥公钥X%d\n", n.X)
starlog.Green("公钥公钥Y%d\n", n.Y)
case *ecdh.PublicKey:
starlog.Green("这是一个ECDH公钥\n")
case *ed25519.PublicKey:
starlog.Green("这是一个ED25519公钥\n")
case *dsa.PublicKey:
starlog.Green("这是一个DSA公钥\n")
starlog.Green("公钥公钥Y%d\n", n.Y)
default:
starlog.Green("未知公钥类型\n")
}
return
default:
starlog.Infof("未知证书文件类型\n")
}
}
}
func keyUsageToStrings(keyUsage x509.KeyUsage) string {
var usages []string
if keyUsage&x509.KeyUsageDigitalSignature != 0 {
usages = append(usages, "Digital Signature")
}
if keyUsage&x509.KeyUsageContentCommitment != 0 {
usages = append(usages, "Content Commitment")
}
if keyUsage&x509.KeyUsageKeyEncipherment != 0 {
usages = append(usages, "Key Encipherment")
}
if keyUsage&x509.KeyUsageDataEncipherment != 0 {
usages = append(usages, "Data Encipherment")
}
if keyUsage&x509.KeyUsageKeyAgreement != 0 {
usages = append(usages, "Key Agreement")
}
if keyUsage&x509.KeyUsageCertSign != 0 {
usages = append(usages, "Certificate Signing")
}
if keyUsage&x509.KeyUsageCRLSign != 0 {
usages = append(usages, "CRL Signing")
}
if keyUsage&x509.KeyUsageEncipherOnly != 0 {
usages = append(usages, "Encipher Only")
}
if keyUsage&x509.KeyUsageDecipherOnly != 0 {
usages = append(usages, "Decipher Only")
}
return strings.Join(usages, ", ")
}
func extKeyUsageToStrings(extKeyUsages []x509.ExtKeyUsage) string {
var usages []string
for _, extKeyUsage := range extKeyUsages {
switch extKeyUsage {
case x509.ExtKeyUsageAny:
usages = append(usages, "Any")
case x509.ExtKeyUsageServerAuth:
usages = append(usages, "Server Authentication")
case x509.ExtKeyUsageClientAuth:
usages = append(usages, "Client Authentication")
case x509.ExtKeyUsageCodeSigning:
usages = append(usages, "Code Signing")
case x509.ExtKeyUsageEmailProtection:
usages = append(usages, "Email Protection")
case x509.ExtKeyUsageIPSECEndSystem:
usages = append(usages, "IPSEC End System")
case x509.ExtKeyUsageIPSECTunnel:
usages = append(usages, "IPSEC Tunnel")
case x509.ExtKeyUsageIPSECUser:
usages = append(usages, "IPSEC User")
case x509.ExtKeyUsageTimeStamping:
usages = append(usages, "Time Stamping")
case x509.ExtKeyUsageOCSPSigning:
usages = append(usages, "OCSP Signing")
case x509.ExtKeyUsageMicrosoftServerGatedCrypto:
usages = append(usages, "Microsoft Server Gated Crypto")
case x509.ExtKeyUsageNetscapeServerGatedCrypto:
usages = append(usages, "Netscape Server Gated Crypto")
default:
usages = append(usages, fmt.Sprintf("Unknown(%d)", extKeyUsage))
}
}
return strings.Join(usages, ", ")
}
func GetCert(data []byte, pwd string) ([]any, []x509.Certificate, error) {
var common []any
var certs []x509.Certificate
{
pems, err := pkcs12.ToPEM(data, pwd)
if err == nil {
for _, v := range pems {
switch v.Type {
case "CERTIFICATE":
cert, err := x509.ParseCertificate(v.Bytes)
if err != nil {
continue
}
starlog.Green("这是一个PKCS12文件\n")
pub := cert.PublicKey
switch pub.(type) {
case *rsa.PublicKey:
starlog.Green("公钥算法为RSA\n")
case *ecdsa.PublicKey:
starlog.Green("公钥算法为ECDSA\n")
case *dsa.PublicKey:
starlog.Green("公钥算法为DSA\n")
case *ecdh.PublicKey:
starlog.Green("公钥算法为ECDH\n")
case *ed25519.PublicKey:
starlog.Green("公钥算法为ED25519\n")
default:
starlog.Green("未知公钥类型\n")
}
common = append(common, pub)
certs = append(certs, *cert)
case "PRIVATE KEY":
priv, err := x509.ParsePKCS8PrivateKey(v.Bytes)
if err != nil {
priv, err = x509.ParsePKCS1PrivateKey(v.Bytes)
if err != nil {
priv, err = x509.ParseECPrivateKey(v.Bytes)
if err != nil {
starlog.Errorf("解析私钥错误:%s\n", err)
continue
} else {
starlog.Green("这是一个ECDSA私钥\n")
}
} else {
starlog.Green("这是一个PKCS1私钥\n")
}
} else {
starlog.Green("这是一个PKCS8私钥\n")
}
starlog.Green("-----私钥信息-----\n\n")
switch n := priv.(type) {
case *rsa.PrivateKey:
starlog.Green("这是一个RSA私钥\n")
starlog.Green("私钥位数:%d\n", n.Size())
case *ecdsa.PrivateKey:
starlog.Green("这是一个ECDSA私钥\n")
starlog.Green("私钥位数:%d\n", n.Curve.Params().BitSize)
case *dsa.PrivateKey:
starlog.Green("这是一个DSA私钥\n")
case *ed25519.PrivateKey:
starlog.Green("这是一个ED25519私钥\n")
case *ecdh.PrivateKey:
starlog.Green("这是一个ECDH私钥\n")
default:
starlog.Green("未知私钥类型\n")
}
common = append(common, priv)
}
}
return common, certs, nil
}
}
idx := 0
for {
idx++
block, rest := pem.Decode(data)
if block == nil {
if idx == 1 {
starlog.Errorf("未知文件类型\n")
return common, certs, errors.New("未知文件类型")
}
return common, certs, nil
}
data = rest
rt := 0
switch block.Type {
case "CERTIFICATE":
rt = 1
starlog.Green("这是一个证书文件\n")
fallthrough
case "CERTIFICATE REQUEST":
if rt == 0 {
starlog.Green("这是一个证书请求文件\n")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
starlog.Errorf("解析证书错误:%s\n", err)
continue
}
common = append(common, cert.PublicKey)
certs = append(certs, *cert)
pub := cert.PublicKey
switch pub.(type) {
case *rsa.PublicKey:
starlog.Green("公钥算法为RSA\n")
case *ecdsa.PublicKey:
starlog.Green("公钥算法为ECDSA\n")
case *dsa.PublicKey:
starlog.Green("公钥算法为DSA\n")
case *ecdh.PublicKey:
starlog.Green("公钥算法为ECDH\n")
case *ed25519.PublicKey:
starlog.Green("公钥算法为ED25519\n")
default:
starlog.Green("未知公钥类型\n")
}
continue
case "PRIVATE KEY":
starlog.Infof("这是一个私钥文件\n")
priv, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
priv, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
priv, err = x509.ParseECPrivateKey(block.Bytes)
if err != nil {
starlog.Errorf("解析私钥错误:%s\n", err)
continue
} else {
starlog.Green("这是一个ECDSA私钥\n")
}
} else {
starlog.Green("这是一个PKCS1私钥\n")
}
} else {
starlog.Green("这是一个PKCS8私钥\n")
}
switch n := priv.(type) {
case *rsa.PrivateKey:
starlog.Green("这是一个RSA私钥\n")
starlog.Green("私钥位数:%d\n", n.Size())
case *ecdsa.PrivateKey:
starlog.Green("这是一个ECDSA私钥\n")
starlog.Green("私钥位数:%d\n", n.Curve.Params().BitSize)
case *dsa.PrivateKey:
starlog.Green("这是一个DSA私钥\n")
case *ed25519.PrivateKey:
starlog.Green("这是一个ED25519私钥\n")
case *ecdh.PrivateKey:
starlog.Green("这是一个ECDH私钥\n")
default:
starlog.Green("未知私钥类型\n")
}
common = append(common, priv)
continue
case "PUBLIC KEY":
starlog.Infof("这是一个公钥文件\n")
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
pub, err = x509.ParsePKCS1PublicKey(block.Bytes)
starlog.Green("这是一个PKCS1公钥\n")
} else {
starlog.Green("这是一个PKIX公钥\n")
}
common = append(common, pub)
switch n := pub.(type) {
case *rsa.PublicKey:
starlog.Green("这是一个RSA公钥\n")
starlog.Green("公钥位数:%d\n", n.Size())
case *ecdsa.PublicKey:
starlog.Green("这是一个ECDSA公钥\n")
starlog.Green("公钥位数:%d\n", n.Curve.Params().BitSize)
case *dsa.PublicKey:
starlog.Green("这是一个DSA公钥\n")
case *ecdh.PublicKey:
starlog.Green("这是一个ECDH公钥\n")
case *ed25519.PublicKey:
starlog.Green("这是一个ED25519公钥\n")
default:
starlog.Green("未知公钥类型\n")
}
return common, certs, nil
default:
starlog.Infof("未知证书文件类型\n")
}
}
}
func Pkcs8(data []byte, pwd string, originName string, outpath string) error {
keys, _, err := GetCert(data, pwd)
if err != nil {
return err
}
for _, v := range keys {
if v == nil {
continue
}
switch n := v.(type) {
case *ecdsa.PrivateKey, *rsa.PrivateKey, *dsa.PrivateKey, *ed25519.PrivateKey, *ecdh.PrivateKey:
data, err = x509.MarshalPKCS8PrivateKey(n)
if err != nil {
return err
}
err = os.WriteFile(outpath+"/"+originName+".pkcs8", pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: data}), 0644)
if err != nil {
return err
} else {
starlog.Green("已将私钥保存到%s\n", outpath+"/"+originName+".pkcs8")
}
case *ecdsa.PublicKey, *rsa.PublicKey, *dsa.PublicKey, *ed25519.PublicKey, *ecdh.PublicKey:
data, err = x509.MarshalPKIXPublicKey(n)
if err != nil {
return err
}
err = os.WriteFile(outpath+"/"+originName+".pub.pkcs8", pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: data}), 0644)
if err != nil {
return err
} else {
starlog.Green("已将公钥保存到%s\n", outpath+"/"+originName+".pub.pkcs8")
}
}
}
return nil
}
func Pkcs1(data []byte, pwd string, originName string, outpath string) error {
keys, _, err := GetCert(data, pwd)
if err != nil {
return err
}
for _, v := range keys {
if v == nil {
continue
}
switch n := v.(type) {
case *rsa.PrivateKey:
data = x509.MarshalPKCS1PrivateKey(n)
if err != nil {
return err
}
err = os.WriteFile(outpath+"/"+originName+".pkcs1", pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: data}), 0644)
if err != nil {
return err
} else {
starlog.Green("已将私钥保存到%s\n", outpath+"/"+originName+".pkcs8")
}
case *rsa.PublicKey:
data = x509.MarshalPKCS1PublicKey(n)
if err != nil {
return err
}
err = os.WriteFile(outpath+"/"+originName+".pub.pkcs1", pem.EncodeToMemory(&pem.Block{Type: "RSA PUBLIC KEY", Bytes: data}), 0644)
if err != nil {
return err
} else {
starlog.Green("已将公钥保存到%s\n", outpath+"/"+originName+".pub.pkcs1")
}
}
}
return nil
}

@ -0,0 +1,220 @@
package cert
import (
"b612.me/starcrypto"
"b612.me/stario"
"b612.me/starlog"
"fmt"
"github.com/spf13/cobra"
"os"
"path/filepath"
"time"
)
var country, province, city, org, orgUnit, name string
var dnsName []string
var start, end time.Time
var startStr, endStr string
var savefolder string
var promptMode bool
var isCa bool
var maxPathLenZero bool
var maxPathLen int
var caKey string
var caCert string
var csr string
var pubKey string
var caKeyPwd string
var passwd string
var Cmd = &cobra.Command{
Use: "cert",
Short: "证书生成与解析",
Long: "证书生成与解析",
}
var CmdCsr = &cobra.Command{
Use: "csr",
Short: "生成证书请求",
Long: "生成证书请求",
Run: func(cmd *cobra.Command, args []string) {
var err error
if promptMode {
if country == "" {
country = stario.MessageBox("请输入国家:", "").MustString()
}
if province == "" {
province = stario.MessageBox("请输入省份:", "").MustString()
}
if city == "" {
city = stario.MessageBox("请输入城市:", "").MustString()
}
if org == "" {
org = stario.MessageBox("请输入组织:", "").MustString()
}
if orgUnit == "" {
orgUnit = stario.MessageBox("请输入组织单位:", "").MustString()
}
if name == "" {
name = stario.MessageBox("请输入通用名称:", "").MustString()
}
if dnsName == nil {
dnsName = stario.MessageBox("请输入dns名称用逗号分割", "").MustSliceString(",")
}
if startStr == "" {
startStr = stario.MessageBox("请输入开始时间:", "").MustString()
}
if endStr == "" {
endStr = stario.MessageBox("请输入结束时间:", "").MustString()
}
}
start, err = time.Parse(time.RFC3339, startStr)
if err != nil {
starlog.Errorln("开始时间格式错误,格式:2006-01-02T15:04:05Z07:00", err)
os.Exit(1)
}
end, err = time.Parse(time.RFC3339, endStr)
if err != nil {
starlog.Errorln("结束时间格式错误,格式:2006-01-02T15:04:05Z07:00", err)
os.Exit(1)
}
csr := outputCsr(GenerateCsr(country, province, city, org, orgUnit, name, dnsName, start, end, isCa, maxPathLenZero, maxPathLen))
err = os.WriteFile(savefolder+"/"+name+".csr", csr, 0644)
if err != nil {
starlog.Errorln("保存csr文件错误", err)
os.Exit(1)
}
starlog.Infoln("保存csr文件成功", savefolder+"/"+name+".csr")
},
}
var CmdGen = &cobra.Command{
Use: "gen",
Short: "生成证书",
Long: "生成证书",
Run: func(cmd *cobra.Command, args []string) {
if caKey == "" {
starlog.Errorln("CA私钥不能为空")
os.Exit(1)
}
if caCert == "" {
starlog.Errorln("CA证书不能为空")
os.Exit(1)
}
if csr == "" {
starlog.Errorln("证书请求不能为空")
os.Exit(1)
}
if pubKey == "" {
starlog.Errorln("证书公钥不能为空")
os.Exit(1)
}
caKeyRaw, caCertRaw, err := LoadCA(caKey, caCert, caKeyPwd)
if err != nil {
starlog.Errorln("加载CA错误", err)
os.Exit(1)
}
csrRaw, err := LoadCsr(csr)
if err != nil {
starlog.Errorln("加载证书请求错误", err)
os.Exit(1)
}
pubKeyByte, err := os.ReadFile(pubKey)
if err != nil {
starlog.Errorln("加载公钥错误", err)
os.Exit(1)
}
pubKeyRaw, err := starcrypto.DecodePublicKey(pubKeyByte)
if err != nil {
starlog.Errorln("解析公钥错误", err)
os.Exit(1)
}
cert, err := MakeCert(caKeyRaw, caCertRaw, csrRaw, pubKeyRaw)
if err != nil {
starlog.Errorln("生成证书错误", err)
os.Exit(1)
}
err = os.WriteFile(savefolder+"/"+csrRaw.Subject.CommonName+".crt", cert, 0644)
if err != nil {
starlog.Errorln("保存证书错误", err)
os.Exit(1)
}
starlog.Infoln("保存证书成功", savefolder+"/"+csrRaw.Subject.CommonName+".crt")
},
}
var CmdParse = &cobra.Command{
Use: "parse",
Short: "解析证书",
Long: "解析证书",
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
starlog.Errorln("请输入证书文件")
os.Exit(1)
}
for _, v := range args {
data, err := os.ReadFile(v)
if err != nil {
starlog.Errorln("读取证书错误", err)
continue
}
ParseCert(data, passwd)
fmt.Println("\n-------" + v + "解析完毕---------\n")
}
},
}
func init() {
Cmd.AddCommand(CmdCsr)
CmdCsr.Flags().BoolVarP(&promptMode, "prompt", "P", false, "是否交互模式")
CmdCsr.Flags().StringVarP(&country, "country", "c", "", "国家")
CmdCsr.Flags().StringVarP(&province, "province", "p", "", "省份")
CmdCsr.Flags().StringVarP(&city, "city", "t", "", "城市")
CmdCsr.Flags().StringVarP(&org, "org", "o", "", "组织")
CmdCsr.Flags().StringVarP(&orgUnit, "orgUnit", "u", "", "组织单位")
CmdCsr.Flags().StringVarP(&name, "name", "n", "", "通用名称")
CmdCsr.Flags().StringSliceVarP(&dnsName, "dnsName", "d", nil, "dns名称")
CmdCsr.Flags().StringVarP(&startStr, "start", "S", time.Now().Format(time.RFC3339), "开始时间,格式:2006-01-02T15:04:05Z07:00")
CmdCsr.Flags().StringVarP(&endStr, "end", "E", time.Now().AddDate(1, 0, 0).Format(time.RFC3339), "结束时间,格式:2006-01-02T15:04:05Z07:00")
CmdCsr.Flags().StringVarP(&savefolder, "savefolder", "s", "./", "保存文件夹")
CmdCsr.Flags().BoolVarP(&isCa, "isCa", "A", false, "是否是CA")
CmdCsr.Flags().BoolVarP(&maxPathLenZero, "maxPathLenZero", "z", false, "允许最大路径长度为0")
CmdCsr.Flags().IntVarP(&maxPathLen, "maxPathLen", "m", 0, "最大路径长度")
CmdGen.Flags().StringVarP(&caKey, "caKey", "k", "", "CA私钥")
CmdGen.Flags().StringVarP(&caCert, "caCert", "C", "", "CA证书")
CmdGen.Flags().StringVarP(&csr, "csr", "r", "", "证书请求")
CmdGen.Flags().StringVarP(&pubKey, "pubKey", "P", "", "证书公钥")
CmdGen.Flags().StringVarP(&savefolder, "savefolder", "s", "./", "保存文件夹")
CmdGen.Flags().StringVarP(&caKeyPwd, "caKeyPwd", "p", "", "CA私钥密码")
Cmd.AddCommand(CmdGen)
CmdParse.Flags().StringVarP(&passwd, "passwd", "p", "", "证书密码")
Cmd.AddCommand(CmdParse)
CmdPkcs8.Flags().StringVarP(&passwd, "passwd", "p", "", "证书密码")
CmdPkcs8.Flags().StringVarP(&savefolder, "savefolder", "s", "./", "保存文件夹")
Cmd.AddCommand(CmdPkcs8)
}
var CmdPkcs8 = &cobra.Command{
Use: "pkcs8",
Short: "pkcs8转换",
Long: "pkcs8转换",
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
starlog.Errorln("请输入证书文件")
os.Exit(1)
}
for _, v := range args {
data, err := os.ReadFile(v)
if err != nil {
starlog.Errorln("读取证书错误", err)
continue
}
Pkcs8(data, passwd, filepath.Base(v), savefolder)
fmt.Println("\n-------" + v + "转换完毕---------\n")
}
},
}

@ -0,0 +1,83 @@
package cert
import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"math/big"
"net"
"os"
"time"
)
func GenerateCsr(country, province, city, org, orgUnit, name string, dnsName []string, start, end time.Time, isCa bool, maxPathLenZero bool, maxPathLen int) *x509.Certificate {
var trueDNS []string
var trueIp []net.IP
for _, v := range dnsName {
ip := net.ParseIP(v)
if ip == nil {
trueDNS = append(trueDNS, v)
continue
}
trueIp = append(trueIp, ip)
}
ku := x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
eku := x509.ExtKeyUsageServerAuth
if isCa {
ku = x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageDigitalSignature
eku = x509.ExtKeyUsageAny
}
return &x509.Certificate{
Version: 3,
SerialNumber: big.NewInt(time.Now().Unix()),
Subject: pkix.Name{
Country: s2s(country),
Province: s2s(province),
Locality: s2s(city),
Organization: s2s((org)),
OrganizationalUnit: s2s(orgUnit),
CommonName: name,
},
DNSNames: trueDNS,
IPAddresses: trueIp,
NotBefore: start,
NotAfter: end,
BasicConstraintsValid: true,
IsCA: isCa,
MaxPathLen: maxPathLen,
MaxPathLenZero: maxPathLenZero,
KeyUsage: ku,
ExtKeyUsage: []x509.ExtKeyUsage{eku},
}
}
func outputCsr(csr *x509.Certificate) []byte {
return pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE REQUEST",
Bytes: csr.Raw,
})
}
func s2s(str string) []string {
if len(str) == 0 {
return nil
}
return []string{str}
}
func LoadCsr(csrPath string) (*x509.Certificate, error) {
csrBytes, err := os.ReadFile(csrPath)
if err != nil {
return nil, err
}
block, _ := pem.Decode(csrBytes)
if block == nil || block.Type != "CERTIFICATE REQUEST" {
return nil, errors.New("Failed to decode PEM block containing the certificate")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
return cert, nil
}

@ -7,12 +7,14 @@ require (
b612.me/starcrypto v0.0.4
b612.me/stario v0.0.9
b612.me/starlog v1.3.3
b612.me/starnet v0.1.8
b612.me/staros v1.1.7
b612.me/starssh v0.0.2
b612.me/startext v0.0.0-20220314043758-22c6d5e5b1cd
b612.me/wincmd v0.0.3
github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2
github.com/emersion/go-smtp v0.20.2
github.com/goftp/file-driver v0.0.0-20180502053751-5d604a0fc0c9
github.com/goftp/server v0.0.0-20200708154336-f64f7c2d8a42
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
@ -22,15 +24,14 @@ require (
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/spf13/cobra v1.8.0
github.com/things-go/go-socks5 v0.0.5
software.sslmate.com/src/go-pkcs12 v0.4.0
)
require (
b612.me/starmap v1.2.4 // indirect
b612.me/starnet v0.1.8 // indirect
b612.me/win32api v0.0.2 // indirect
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
github.com/emersion/go-smtp v0.20.2 // indirect
github.com/jlaffaye/ftp v0.1.0 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/pkg/sftp v1.13.4 // indirect

@ -133,3 +133,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=

@ -17,7 +17,7 @@ import (
"time"
)
var version = "2.0.1"
var version = "2.1.0"
func (h *ReverseConfig) Run() error {
err := h.init()

@ -22,7 +22,7 @@ import (
"time"
)
var version = "2.0.1"
var version = "2.1.0"
type HttpServerCfgs func(cfg *HttpServerCfg)

@ -4,6 +4,8 @@ import (
"b612.me/starcrypto"
"b612.me/starlog"
"b612.me/staros"
"crypto/ecdsa"
"crypto/rsa"
"github.com/spf13/cobra"
"os"
"time"
@ -18,6 +20,8 @@ var path string
var key string
var outpath string
var sshPub bool
func init() {
Cmd.Flags().StringVarP(&k.Type, "type", "t", "rsa", "Key Type: rsa, ecdsa")
Cmd.Flags().StringVarP(&k.Encrypt, "encrypt", "e", "", "Encrypt Key with Password (not recommended)")
@ -39,6 +43,11 @@ func init() {
CmdEn.Flags().StringVarP(&outpath, "outpath", "o", "./newkey", "new key file output path")
Cmd.AddCommand(CmdEn)
CmdPub.Flags().StringVarP(&path, "path", "p", "", "private key file path")
CmdPub.Flags().StringVarP(&outpath, "outpath", "o", "./public.key", "public key file output path")
CmdPub.Flags().BoolVarP(&sshPub, "ssh", "s", false, "output ssh public key")
Cmd.AddCommand(CmdPub)
}
var Cmd = &cobra.Command{
@ -102,3 +111,56 @@ var CmdEn = &cobra.Command{
starlog.Infoln("new key saved to", outpath)
},
}
var CmdPub = &cobra.Command{
Use: "pub",
Short: "通过私钥生成公钥",
Run: func(cmd *cobra.Command, args []string) {
var pub any
if !staros.Exists(path) {
starlog.Errorln("file not exists")
os.Exit(1)
}
data, err := os.ReadFile(path)
if err != nil {
starlog.Errorln("read file error:", err)
os.Exit(1)
}
priv, err := starcrypto.DecodePrivateKey(data, key)
if err != nil {
starlog.Errorln("decode private key error:", err)
os.Exit(1)
}
switch n := priv.(type) {
case *rsa.PrivateKey:
starlog.Infoln("found rsa private key")
pub = n.Public()
case *ecdsa.PrivateKey:
starlog.Infoln("found ecdsa private key")
pub = n.Public()
default:
starlog.Errorln("unknown private key type")
os.Exit(1)
}
if sshPub {
data, err = starcrypto.EncodeSSHPublicKey(pub)
if err != nil {
starlog.Errorln("encode ssh public key error:", err)
os.Exit(1)
}
} else {
data, err = starcrypto.EncodePublicKey(pub)
if err != nil {
starlog.Errorln("encode public key error:", err)
os.Exit(1)
}
}
starlog.Infoln("public key:", string(data))
err = os.WriteFile(outpath, data, 0644)
if err != nil {
starlog.Errorln("write public key error:", err)
os.Exit(1)
}
starlog.Infoln("public key saved to", outpath)
},
}

@ -6,6 +6,7 @@ import (
"b612.me/apps/b612/base85"
"b612.me/apps/b612/base91"
"b612.me/apps/b612/calc"
"b612.me/apps/b612/cert"
"b612.me/apps/b612/detach"
"b612.me/apps/b612/df"
"b612.me/apps/b612/dfinder"
@ -22,6 +23,7 @@ import (
"b612.me/apps/b612/net"
"b612.me/apps/b612/rmt"
"b612.me/apps/b612/search"
"b612.me/apps/b612/smtpclient"
"b612.me/apps/b612/smtpserver"
"b612.me/apps/b612/socks5"
"b612.me/apps/b612/split"
@ -37,7 +39,7 @@ import (
var cmdRoot = &cobra.Command{
Use: "b612",
Version: "2.1.0.alpha",
Version: "2.1.0.beta",
}
func init() {
@ -45,7 +47,8 @@ func init() {
cmdRoot.AddCommand(tcping.Cmd, uac.Cmd, httpserver.Cmd, httpreverse.Cmd,
base64.Cmd, base85.Cmd, base91.Cmd, attach.Cmd, detach.Cmd, df.Cmd, dfinder.Cmd,
ftp.Cmd, generate.Cmd, hash.Cmd, image.Cmd, merge.Cmd, search.Cmd, split.Cmd, vic.Cmd,
calc.Cmd, net.Cmd, rmt.Cmds, rmt.Cmdc, keygen.Cmd, dns.Cmd, whois.Cmd, socks5.Cmd, httproxy.Cmd, smtpserver.Cmd)
calc.Cmd, net.Cmd, rmt.Cmds, rmt.Cmdc, keygen.Cmd, dns.Cmd, whois.Cmd, socks5.Cmd, httproxy.Cmd, smtpserver.Cmd, smtpclient.Cmd,
cert.Cmd)
}
func main() {

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 Jordan Wright
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,809 @@
// Package email is designed to provide an "email interface for humans."
// Designed to be robust and flexible, the email package aims to make sending email easy without getting in the way.
package email
import (
"bufio"
"bytes"
"crypto/rand"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"io"
"math"
"math/big"
"mime"
"mime/multipart"
"mime/quotedprintable"
"net/mail"
"net/smtp"
"net/textproto"
"os"
"path/filepath"
"strings"
"time"
"unicode"
)
const (
MaxLineLength = 76 // MaxLineLength is the maximum line length per RFC 2045
defaultContentType = "text/plain; charset=us-ascii" // defaultContentType is the default Content-Type according to RFC 2045, section 5.2
)
// ErrMissingBoundary is returned when there is no boundary given for a multipart entity
var ErrMissingBoundary = errors.New("No boundary found for multipart entity")
// ErrMissingContentType is returned when there is no "Content-Type" header for a MIME entity
var ErrMissingContentType = errors.New("No Content-Type found for MIME entity")
// Email is the type used for email messages
type Email struct {
ReplyTo []string
From string
To []string
Bcc []string
Cc []string
Subject string
Text []byte // Plaintext message (optional)
HTML []byte // Html message (optional)
Sender string // override From as SMTP envelope sender (optional)
Headers textproto.MIMEHeader
Attachments []*Attachment
ReadReceipt []string
}
// part is a copyable representation of a multipart.Part
type part struct {
header textproto.MIMEHeader
body []byte
}
// NewEmail creates an Email, and returns the pointer to it.
func NewEmail() *Email {
return &Email{Headers: textproto.MIMEHeader{}}
}
// trimReader is a custom io.Reader that will trim any leading
// whitespace, as this can cause email imports to fail.
type trimReader struct {
rd io.Reader
trimmed bool
}
// Read trims off any unicode whitespace from the originating reader
func (tr *trimReader) Read(buf []byte) (int, error) {
n, err := tr.rd.Read(buf)
if err != nil {
return n, err
}
if !tr.trimmed {
t := bytes.TrimLeftFunc(buf[:n], unicode.IsSpace)
tr.trimmed = true
n = copy(buf, t)
}
return n, err
}
func handleAddressList(v []string) []string {
res := []string{}
for _, a := range v {
w := strings.Split(a, ",")
for _, addr := range w {
decodedAddr, err := (&mime.WordDecoder{}).DecodeHeader(strings.TrimSpace(addr))
if err == nil {
res = append(res, decodedAddr)
} else {
res = append(res, addr)
}
}
}
return res
}
// NewEmailFromReader reads a stream of bytes from an io.Reader, r,
// and returns an email struct containing the parsed data.
// This function expects the data in RFC 5322 format.
func NewEmailFromReader(r io.Reader) (*Email, error) {
e := NewEmail()
s := &trimReader{rd: r}
tp := textproto.NewReader(bufio.NewReader(s))
// Parse the main headers
hdrs, err := tp.ReadMIMEHeader()
if err != nil {
return e, err
}
// Set the subject, to, cc, bcc, and from
for h, v := range hdrs {
switch h {
case "Subject":
e.Subject = v[0]
subj, err := (&mime.WordDecoder{}).DecodeHeader(e.Subject)
if err == nil && len(subj) > 0 {
e.Subject = subj
}
delete(hdrs, h)
case "To":
e.To = handleAddressList(v)
delete(hdrs, h)
case "Cc":
e.Cc = handleAddressList(v)
delete(hdrs, h)
case "Bcc":
e.Bcc = handleAddressList(v)
delete(hdrs, h)
case "Reply-To":
e.ReplyTo = handleAddressList(v)
delete(hdrs, h)
case "From":
e.From = v[0]
fr, err := (&mime.WordDecoder{}).DecodeHeader(e.From)
if err == nil && len(fr) > 0 {
e.From = fr
}
delete(hdrs, h)
}
}
e.Headers = hdrs
body := tp.R
// Recursively parse the MIME parts
ps, err := parseMIMEParts(e.Headers, body)
if err != nil {
return e, err
}
for _, p := range ps {
if ct := p.header.Get("Content-Type"); ct == "" {
return e, ErrMissingContentType
}
ct, _, err := mime.ParseMediaType(p.header.Get("Content-Type"))
if err != nil {
return e, err
}
// Check if part is an attachment based on the existence of the Content-Disposition header with a value of "attachment".
if cd := p.header.Get("Content-Disposition"); cd != "" {
cd, params, err := mime.ParseMediaType(p.header.Get("Content-Disposition"))
if err != nil {
return e, err
}
filename, filenameDefined := params["filename"]
if cd == "attachment" || (cd == "inline" && filenameDefined) {
_, err = e.Attach(bytes.NewReader(p.body), filename, ct)
if err != nil {
return e, err
}
continue
}
}
switch {
case ct == "text/plain":
e.Text = p.body
case ct == "text/html":
e.HTML = p.body
}
}
return e, nil
}
// parseMIMEParts will recursively walk a MIME entity and return a []mime.Part containing
// each (flattened) mime.Part found.
// It is important to note that there are no limits to the number of recursions, so be
// careful when parsing unknown MIME structures!
func parseMIMEParts(hs textproto.MIMEHeader, b io.Reader) ([]*part, error) {
var ps []*part
// If no content type is given, set it to the default
if _, ok := hs["Content-Type"]; !ok {
hs.Set("Content-Type", defaultContentType)
}
ct, params, err := mime.ParseMediaType(hs.Get("Content-Type"))
if err != nil {
return ps, err
}
// If it's a multipart email, recursively parse the parts
if strings.HasPrefix(ct, "multipart/") {
if _, ok := params["boundary"]; !ok {
return ps, ErrMissingBoundary
}
mr := multipart.NewReader(b, params["boundary"])
for {
var buf bytes.Buffer
p, err := mr.NextPart()
if err == io.EOF {
break
}
if err != nil {
return ps, err
}
if _, ok := p.Header["Content-Type"]; !ok {
p.Header.Set("Content-Type", defaultContentType)
}
subct, _, err := mime.ParseMediaType(p.Header.Get("Content-Type"))
if err != nil {
return ps, err
}
if strings.HasPrefix(subct, "multipart/") {
sps, err := parseMIMEParts(p.Header, p)
if err != nil {
return ps, err
}
ps = append(ps, sps...)
} else {
var reader io.Reader
reader = p
const cte = "Content-Transfer-Encoding"
if p.Header.Get(cte) == "base64" {
reader = base64.NewDecoder(base64.StdEncoding, reader)
}
// Otherwise, just append the part to the list
// Copy the part data into the buffer
if _, err := io.Copy(&buf, reader); err != nil {
return ps, err
}
ps = append(ps, &part{body: buf.Bytes(), header: p.Header})
}
}
} else {
// If it is not a multipart email, parse the body content as a single "part"
switch hs.Get("Content-Transfer-Encoding") {
case "quoted-printable":
b = quotedprintable.NewReader(b)
case "base64":
b = base64.NewDecoder(base64.StdEncoding, b)
}
var buf bytes.Buffer
if _, err := io.Copy(&buf, b); err != nil {
return ps, err
}
ps = append(ps, &part{body: buf.Bytes(), header: hs})
}
return ps, nil
}
// Attach is used to attach content from an io.Reader to the email.
// Required parameters include an io.Reader, the desired filename for the attachment, and the Content-Type
// The function will return the created Attachment for reference, as well as nil for the error, if successful.
func (e *Email) Attach(r io.Reader, filename string, c string) (a *Attachment, err error) {
var buffer bytes.Buffer
if _, err = io.Copy(&buffer, r); err != nil {
return
}
at := &Attachment{
Filename: filename,
ContentType: c,
Header: textproto.MIMEHeader{},
Content: buffer.Bytes(),
}
e.Attachments = append(e.Attachments, at)
return at, nil
}
// AttachFile is used to attach content to the email.
// It attempts to open the file referenced by filename and, if successful, creates an Attachment.
// This Attachment is then appended to the slice of Email.Attachments.
// The function will then return the Attachment for reference, as well as nil for the error, if successful.
func (e *Email) AttachFile(filename string) (a *Attachment, err error) {
f, err := os.Open(filename)
if err != nil {
return
}
defer f.Close()
ct := mime.TypeByExtension(filepath.Ext(filename))
basename := filepath.Base(filename)
return e.Attach(f, basename, ct)
}
// msgHeaders merges the Email's various fields and custom headers together in a
// standards compliant way to create a MIMEHeader to be used in the resulting
// message. It does not alter e.Headers.
//
// "e"'s fields To, Cc, From, Subject will be used unless they are present in
// e.Headers. Unless set in e.Headers, "Date" will filled with the current time.
func (e *Email) msgHeaders() (textproto.MIMEHeader, error) {
res := make(textproto.MIMEHeader, len(e.Headers)+6)
if e.Headers != nil {
for _, h := range []string{"Reply-To", "To", "Cc", "From", "Subject", "Date", "Message-Id", "MIME-Version"} {
if v, ok := e.Headers[h]; ok {
res[h] = v
}
}
}
// Set headers if there are values.
if _, ok := res["Reply-To"]; !ok && len(e.ReplyTo) > 0 {
res.Set("Reply-To", strings.Join(e.ReplyTo, ", "))
}
if _, ok := res["To"]; !ok && len(e.To) > 0 {
res.Set("To", strings.Join(e.To, ", "))
}
if _, ok := res["Cc"]; !ok && len(e.Cc) > 0 {
res.Set("Cc", strings.Join(e.Cc, ", "))
}
if _, ok := res["Subject"]; !ok && e.Subject != "" {
res.Set("Subject", e.Subject)
}
if _, ok := res["Message-Id"]; !ok {
id, err := generateMessageID()
if err != nil {
return nil, err
}
res.Set("Message-Id", id)
}
// Date and From are required headers.
if _, ok := res["From"]; !ok {
res.Set("From", e.From)
}
if _, ok := res["Date"]; !ok {
res.Set("Date", time.Now().Format(time.RFC1123Z))
}
if _, ok := res["MIME-Version"]; !ok {
res.Set("MIME-Version", "1.0")
}
for field, vals := range e.Headers {
if _, ok := res[field]; !ok {
res[field] = vals
}
}
return res, nil
}
func writeMessage(buff io.Writer, msg []byte, multipart bool, mediaType string, w *multipart.Writer) error {
if multipart {
header := textproto.MIMEHeader{
"Content-Type": {mediaType + "; charset=UTF-8"},
"Content-Transfer-Encoding": {"quoted-printable"},
}
if _, err := w.CreatePart(header); err != nil {
return err
}
}
qp := quotedprintable.NewWriter(buff)
// Write the text
if _, err := qp.Write(msg); err != nil {
return err
}
return qp.Close()
}
func (e *Email) categorizeAttachments() (htmlRelated, others []*Attachment) {
for _, a := range e.Attachments {
if a.HTMLRelated {
htmlRelated = append(htmlRelated, a)
} else {
others = append(others, a)
}
}
return
}
// Bytes converts the Email object to a []byte representation, including all needed MIMEHeaders, boundaries, etc.
func (e *Email) Bytes() ([]byte, error) {
// TODO: better guess buffer size
buff := bytes.NewBuffer(make([]byte, 0, 4096))
headers, err := e.msgHeaders()
if err != nil {
return nil, err
}
htmlAttachments, otherAttachments := e.categorizeAttachments()
if len(e.HTML) == 0 && len(htmlAttachments) > 0 {
return nil, errors.New("there are HTML attachments, but no HTML body")
}
var (
isMixed = len(otherAttachments) > 0
isAlternative = len(e.Text) > 0 && len(e.HTML) > 0
isRelated = len(e.HTML) > 0 && len(htmlAttachments) > 0
)
var w *multipart.Writer
if isMixed || isAlternative || isRelated {
w = multipart.NewWriter(buff)
}
switch {
case isMixed:
headers.Set("Content-Type", "multipart/mixed;\r\n boundary="+w.Boundary())
case isAlternative:
headers.Set("Content-Type", "multipart/alternative;\r\n boundary="+w.Boundary())
case isRelated:
headers.Set("Content-Type", "multipart/related;\r\n boundary="+w.Boundary())
case len(e.HTML) > 0:
headers.Set("Content-Type", "text/html; charset=UTF-8")
headers.Set("Content-Transfer-Encoding", "quoted-printable")
default:
headers.Set("Content-Type", "text/plain; charset=UTF-8")
headers.Set("Content-Transfer-Encoding", "quoted-printable")
}
headerToBytes(buff, headers)
_, err = io.WriteString(buff, "\r\n")
if err != nil {
return nil, err
}
// Check to see if there is a Text or HTML field
if len(e.Text) > 0 || len(e.HTML) > 0 {
var subWriter *multipart.Writer
if isMixed && isAlternative {
// Create the multipart alternative part
subWriter = multipart.NewWriter(buff)
header := textproto.MIMEHeader{
"Content-Type": {"multipart/alternative;\r\n boundary=" + subWriter.Boundary()},
}
if _, err := w.CreatePart(header); err != nil {
return nil, err
}
} else {
subWriter = w
}
// Create the body sections
if len(e.Text) > 0 {
// Write the text
if err := writeMessage(buff, e.Text, isMixed || isAlternative, "text/plain", subWriter); err != nil {
return nil, err
}
}
if len(e.HTML) > 0 {
messageWriter := subWriter
var relatedWriter *multipart.Writer
if (isMixed || isAlternative) && len(htmlAttachments) > 0 {
relatedWriter = multipart.NewWriter(buff)
header := textproto.MIMEHeader{
"Content-Type": {"multipart/related;\r\n boundary=" + relatedWriter.Boundary()},
}
if _, err := subWriter.CreatePart(header); err != nil {
return nil, err
}
messageWriter = relatedWriter
} else if isRelated && len(htmlAttachments) > 0 {
relatedWriter = w
messageWriter = w
}
// Write the HTML
if err := writeMessage(buff, e.HTML, isMixed || isAlternative || isRelated, "text/html", messageWriter); err != nil {
return nil, err
}
if len(htmlAttachments) > 0 {
for _, a := range htmlAttachments {
a.setDefaultHeaders()
ap, err := relatedWriter.CreatePart(a.Header)
if err != nil {
return nil, err
}
// Write the base64Wrapped content to the part
base64Wrap(ap, a.Content)
}
if isMixed || isAlternative {
relatedWriter.Close()
}
}
}
if isMixed && isAlternative {
if err := subWriter.Close(); err != nil {
return nil, err
}
}
}
// Create attachment part, if necessary
for _, a := range otherAttachments {
a.setDefaultHeaders()
ap, err := w.CreatePart(a.Header)
if err != nil {
return nil, err
}
// Write the base64Wrapped content to the part
base64Wrap(ap, a.Content)
}
if isMixed || isAlternative || isRelated {
if err := w.Close(); err != nil {
return nil, err
}
}
return buff.Bytes(), nil
}
// Send an email using the given host and SMTP auth (optional), returns any error thrown by smtp.SendMail
// This function merges the To, Cc, and Bcc fields and calls the smtp.SendMail function using the Email.Bytes() output as the message
func (e *Email) Send(addr string, a smtp.Auth) error {
// Merge the To, Cc, and Bcc fields
to := make([]string, 0, len(e.To)+len(e.Cc)+len(e.Bcc))
to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
for i := 0; i < len(to); i++ {
addr, err := mail.ParseAddress(to[i])
if err != nil {
return err
}
to[i] = addr.Address
}
// Check to make sure there is at least one recipient and one "From" address
if e.From == "" || len(to) == 0 {
return errors.New("Must specify at least one From address and one To address")
}
sender, err := e.parseSender()
if err != nil {
return err
}
raw, err := e.Bytes()
if err != nil {
return err
}
return smtp.SendMail(addr, a, sender, to, raw)
}
// Select and parse an SMTP envelope sender address. Choose Email.Sender if set, or fallback to Email.From.
func (e *Email) parseSender() (string, error) {
if e.Sender != "" {
sender, err := mail.ParseAddress(e.Sender)
if err != nil {
return "", err
}
return sender.Address, nil
} else {
from, err := mail.ParseAddress(e.From)
if err != nil {
return "", err
}
return from.Address, nil
}
}
// SendWithTLS sends an email over tls with an optional TLS config.
//
// The TLS Config is helpful if you need to connect to a host that is used an untrusted
// certificate.
func (e *Email) SendWithTLS(addr string, a smtp.Auth, t *tls.Config) error {
// Merge the To, Cc, and Bcc fields
to := make([]string, 0, len(e.To)+len(e.Cc)+len(e.Bcc))
to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
for i := 0; i < len(to); i++ {
addr, err := mail.ParseAddress(to[i])
if err != nil {
return err
}
to[i] = addr.Address
}
// Check to make sure there is at least one recipient and one "From" address
if e.From == "" || len(to) == 0 {
return errors.New("Must specify at least one From address and one To address")
}
sender, err := e.parseSender()
if err != nil {
return err
}
raw, err := e.Bytes()
if err != nil {
return err
}
conn, err := tls.Dial("tcp", addr, t)
if err != nil {
return err
}
c, err := smtp.NewClient(conn, t.ServerName)
if err != nil {
return err
}
defer c.Close()
if err = c.Hello("localhost"); err != nil {
return err
}
if a != nil {
if ok, _ := c.Extension("AUTH"); ok {
if err = c.Auth(a); err != nil {
return err
}
}
}
if err = c.Mail(sender); err != nil {
return err
}
for _, addr := range to {
if err = c.Rcpt(addr); err != nil {
return err
}
}
w, err := c.Data()
if err != nil {
return err
}
_, err = w.Write(raw)
if err != nil {
return err
}
err = w.Close()
if err != nil {
return err
}
return c.Quit()
}
// SendWithStartTLS sends an email over TLS using STARTTLS with an optional TLS config.
//
// The TLS Config is helpful if you need to connect to a host that is used an untrusted
// certificate.
func (e *Email) SendWithStartTLS(addr string, a smtp.Auth, t *tls.Config) error {
// Merge the To, Cc, and Bcc fields
to := make([]string, 0, len(e.To)+len(e.Cc)+len(e.Bcc))
to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
for i := 0; i < len(to); i++ {
addr, err := mail.ParseAddress(to[i])
if err != nil {
return err
}
to[i] = addr.Address
}
// Check to make sure there is at least one recipient and one "From" address
if e.From == "" || len(to) == 0 {
return errors.New("Must specify at least one From address and one To address")
}
sender, err := e.parseSender()
if err != nil {
return err
}
raw, err := e.Bytes()
if err != nil {
return err
}
// Taken from the standard library
// https://github.com/golang/go/blob/master/src/net/smtp/smtp.go#L328
c, err := smtp.Dial(addr)
if err != nil {
return err
}
defer c.Close()
if err = c.Hello("localhost"); err != nil {
return err
}
// Use TLS if available
if ok, _ := c.Extension("STARTTLS"); ok {
if err = c.StartTLS(t); err != nil {
return err
}
}
if a != nil {
if ok, _ := c.Extension("AUTH"); ok {
if err = c.Auth(a); err != nil {
return err
}
}
}
if err = c.Mail(sender); err != nil {
return err
}
for _, addr := range to {
if err = c.Rcpt(addr); err != nil {
return err
}
}
w, err := c.Data()
if err != nil {
return err
}
_, err = w.Write(raw)
if err != nil {
return err
}
err = w.Close()
if err != nil {
return err
}
return c.Quit()
}
// Attachment is a struct representing an email attachment.
// Based on the mime/multipart.FileHeader struct, Attachment contains the name, MIMEHeader, and content of the attachment in question
type Attachment struct {
Filename string
ContentType string
Header textproto.MIMEHeader
Content []byte
HTMLRelated bool
}
func (at *Attachment) setDefaultHeaders() {
contentType := "application/octet-stream"
if len(at.ContentType) > 0 {
contentType = at.ContentType
}
at.Header.Set("Content-Type", contentType)
if len(at.Header.Get("Content-Disposition")) == 0 {
disposition := "attachment"
if at.HTMLRelated {
disposition = "inline"
}
at.Header.Set("Content-Disposition", fmt.Sprintf("%s;\r\n filename=\"%s\"", disposition, at.Filename))
}
if len(at.Header.Get("Content-ID")) == 0 {
at.Header.Set("Content-ID", fmt.Sprintf("<%s>", at.Filename))
}
if len(at.Header.Get("Content-Transfer-Encoding")) == 0 {
at.Header.Set("Content-Transfer-Encoding", "base64")
}
}
// base64Wrap encodes the attachment content, and wraps it according to RFC 2045 standards (every 76 chars)
// The output is then written to the specified io.Writer
func base64Wrap(w io.Writer, b []byte) {
// 57 raw bytes per 76-byte base64 line.
const maxRaw = 57
// Buffer for each line, including trailing CRLF.
buffer := make([]byte, MaxLineLength+len("\r\n"))
copy(buffer[MaxLineLength:], "\r\n")
// Process raw chunks until there's no longer enough to fill a line.
for len(b) >= maxRaw {
base64.StdEncoding.Encode(buffer, b[:maxRaw])
w.Write(buffer)
b = b[maxRaw:]
}
// Handle the last chunk of bytes.
if len(b) > 0 {
out := buffer[:base64.StdEncoding.EncodedLen(len(b))]
base64.StdEncoding.Encode(out, b)
out = append(out, "\r\n"...)
w.Write(out)
}
}
// headerToBytes renders "header" to "buff". If there are multiple values for a
// field, multiple "Field: value\r\n" lines will be emitted.
func headerToBytes(buff io.Writer, header textproto.MIMEHeader) {
for field, vals := range header {
for _, subval := range vals {
// bytes.Buffer.Write() never returns an error.
io.WriteString(buff, field)
io.WriteString(buff, ": ")
// Write the encoded header if needed
switch {
case field == "Content-Type" || field == "Content-Disposition":
buff.Write([]byte(subval))
case field == "From" || field == "To" || field == "Cc" || field == "Bcc":
participants := strings.Split(subval, ",")
for i, v := range participants {
addr, err := mail.ParseAddress(v)
if err != nil {
continue
}
participants[i] = addr.String()
}
buff.Write([]byte(strings.Join(participants, ", ")))
default:
buff.Write([]byte(mime.QEncoding.Encode("UTF-8", subval)))
}
io.WriteString(buff, "\r\n")
}
}
}
var maxBigInt = big.NewInt(math.MaxInt64)
// generateMessageID generates and returns a string suitable for an RFC 2822
// compliant Message-ID, e.g.:
// <1444789264909237300.3464.1819418242800517193@DESKTOP01>
//
// The following parameters are used to generate a Message-ID:
// - The nanoseconds since Epoch
// - The calling PID
// - A cryptographically random int64
// - The sending hostname
func generateMessageID() (string, error) {
t := time.Now().UnixNano()
pid := os.Getpid()
rint, err := rand.Int(rand.Reader, maxBigInt)
if err != nil {
return "", err
}
h, err := os.Hostname()
// If we can't get the hostname, we'll use localhost
if err != nil {
h = "localhost.localdomain"
}
msgid := fmt.Sprintf("<%d.%d.%d@%s>", t, pid, rint, h)
return msgid, nil
}

@ -0,0 +1,933 @@
package email
import (
"fmt"
"strings"
"testing"
"bufio"
"bytes"
"crypto/rand"
"io"
"io/ioutil"
"mime"
"mime/multipart"
"mime/quotedprintable"
"net/mail"
"net/smtp"
"net/textproto"
)
func prepareEmail() *Email {
e := NewEmail()
e.From = "Jordan Wright <test@example.com>"
e.To = []string{"test@example.com"}
e.Bcc = []string{"test_bcc@example.com"}
e.Cc = []string{"test_cc@example.com"}
e.Subject = "Awesome Subject"
return e
}
func basicTests(t *testing.T, e *Email) *mail.Message {
raw, err := e.Bytes()
if err != nil {
t.Fatal("Failed to render message: ", e)
}
msg, err := mail.ReadMessage(bytes.NewBuffer(raw))
if err != nil {
t.Fatal("Could not parse rendered message: ", err)
}
expectedHeaders := map[string]string{
"To": "<test@example.com>",
"From": "\"Jordan Wright\" <test@example.com>",
"Cc": "<test_cc@example.com>",
"Subject": "Awesome Subject",
}
for header, expected := range expectedHeaders {
if val := msg.Header.Get(header); val != expected {
t.Errorf("Wrong value for message header %s: %v != %v", header, expected, val)
}
}
return msg
}
func TestEmailText(t *testing.T) {
e := prepareEmail()
e.Text = []byte("Text Body is, of course, supported!\n")
msg := basicTests(t, e)
// Were the right headers set?
ct := msg.Header.Get("Content-type")
mt, _, err := mime.ParseMediaType(ct)
if err != nil {
t.Fatal("Content-type header is invalid: ", ct)
} else if mt != "text/plain" {
t.Fatalf("Content-type expected \"text/plain\", not %v", mt)
}
}
func TestEmailWithHTMLAttachments(t *testing.T) {
e := prepareEmail()
// Set plain text to exercise "mime/alternative"
e.Text = []byte("Text Body is, of course, supported!\n")
e.HTML = []byte("<html><body>This is a text.</body></html>")
// Set HTML attachment to exercise "mime/related".
attachment, err := e.Attach(bytes.NewBufferString("Rad attachment"), "rad.txt", "image/png; charset=utf-8")
if err != nil {
t.Fatal("Could not add an attachment to the message: ", err)
}
attachment.HTMLRelated = true
b, err := e.Bytes()
if err != nil {
t.Fatal("Could not serialize e-mail:", err)
}
// Print the bytes for ocular validation and make sure no errors.
//fmt.Println(string(b))
// TODO: Verify the attachments.
s := &trimReader{rd: bytes.NewBuffer(b)}
tp := textproto.NewReader(bufio.NewReader(s))
// Parse the main headers
hdrs, err := tp.ReadMIMEHeader()
if err != nil {
t.Fatal("Could not parse the headers:", err)
}
// Recursively parse the MIME parts
ps, err := parseMIMEParts(hdrs, tp.R)
if err != nil {
t.Fatal("Could not parse the MIME parts recursively:", err)
}
plainTextFound := false
htmlFound := false
imageFound := false
if expected, actual := 3, len(ps); actual != expected {
t.Error("Unexpected number of parts. Expected:", expected, "Was:", actual)
}
for _, part := range ps {
// part has "header" and "body []byte"
cd := part.header.Get("Content-Disposition")
ct := part.header.Get("Content-Type")
if strings.Contains(ct, "image/png") && strings.HasPrefix(cd, "inline") {
imageFound = true
}
if strings.Contains(ct, "text/html") {
htmlFound = true
}
if strings.Contains(ct, "text/plain") {
plainTextFound = true
}
}
if !plainTextFound {
t.Error("Did not find plain text part.")
}
if !htmlFound {
t.Error("Did not find HTML part.")
}
if !imageFound {
t.Error("Did not find image part.")
}
}
func TestEmailWithHTMLAttachmentsHTMLOnly(t *testing.T) {
e := prepareEmail()
e.HTML = []byte("<html><body>This is a text.</body></html>")
// Set HTML attachment to exercise "mime/related".
attachment, err := e.Attach(bytes.NewBufferString("Rad attachment"), "rad.txt", "image/png; charset=utf-8")
if err != nil {
t.Fatal("Could not add an attachment to the message: ", err)
}
attachment.HTMLRelated = true
b, err := e.Bytes()
if err != nil {
t.Fatal("Could not serialize e-mail:", err)
}
// Print the bytes for ocular validation and make sure no errors.
//fmt.Println(string(b))
// TODO: Verify the attachments.
s := &trimReader{rd: bytes.NewBuffer(b)}
tp := textproto.NewReader(bufio.NewReader(s))
// Parse the main headers
hdrs, err := tp.ReadMIMEHeader()
if err != nil {
t.Fatal("Could not parse the headers:", err)
}
if !strings.HasPrefix(hdrs.Get("Content-Type"), "multipart/related") {
t.Error("Envelope Content-Type is not multipart/related: ", hdrs["Content-Type"])
}
// Recursively parse the MIME parts
ps, err := parseMIMEParts(hdrs, tp.R)
if err != nil {
t.Fatal("Could not parse the MIME parts recursively:", err)
}
htmlFound := false
imageFound := false
if expected, actual := 2, len(ps); actual != expected {
t.Error("Unexpected number of parts. Expected:", expected, "Was:", actual)
}
for _, part := range ps {
// part has "header" and "body []byte"
ct := part.header.Get("Content-Type")
if strings.Contains(ct, "image/png") {
imageFound = true
}
if strings.Contains(ct, "text/html") {
htmlFound = true
}
}
if !htmlFound {
t.Error("Did not find HTML part.")
}
if !imageFound {
t.Error("Did not find image part.")
}
}
func TestEmailHTML(t *testing.T) {
e := prepareEmail()
e.HTML = []byte("<h1>Fancy Html is supported, too!</h1>\n")
msg := basicTests(t, e)
// Were the right headers set?
ct := msg.Header.Get("Content-type")
mt, _, err := mime.ParseMediaType(ct)
if err != nil {
t.Fatalf("Content-type header is invalid: %#v", ct)
} else if mt != "text/html" {
t.Fatalf("Content-type expected \"text/html\", not %v", mt)
}
}
func TestEmailTextAttachment(t *testing.T) {
e := prepareEmail()
e.Text = []byte("Text Body is, of course, supported!\n")
_, err := e.Attach(bytes.NewBufferString("Rad attachment"), "rad.txt", "text/plain; charset=utf-8")
if err != nil {
t.Fatal("Could not add an attachment to the message: ", err)
}
msg := basicTests(t, e)
// Were the right headers set?
ct := msg.Header.Get("Content-type")
mt, params, err := mime.ParseMediaType(ct)
if err != nil {
t.Fatal("Content-type header is invalid: ", ct)
} else if mt != "multipart/mixed" {
t.Fatalf("Content-type expected \"multipart/mixed\", not %v", mt)
}
b := params["boundary"]
if b == "" {
t.Fatalf("Invalid or missing boundary parameter: %#v", b)
}
if len(params) != 1 {
t.Fatal("Unexpected content-type parameters")
}
// Is the generated message parsable?
mixed := multipart.NewReader(msg.Body, params["boundary"])
text, err := mixed.NextPart()
if err != nil {
t.Fatalf("Could not find text component of email: %s", err)
}
// Does the text portion match what we expect?
mt, _, err = mime.ParseMediaType(text.Header.Get("Content-type"))
if err != nil {
t.Fatal("Could not parse message's Content-Type")
} else if mt != "text/plain" {
t.Fatal("Message missing text/plain")
}
plainText, err := ioutil.ReadAll(text)
if err != nil {
t.Fatal("Could not read plain text component of message: ", err)
}
if !bytes.Equal(plainText, []byte("Text Body is, of course, supported!\r\n")) {
t.Fatalf("Plain text is broken: %#q", plainText)
}
// Check attachments.
_, err = mixed.NextPart()
if err != nil {
t.Fatalf("Could not find attachment component of email: %s", err)
}
if _, err = mixed.NextPart(); err != io.EOF {
t.Error("Expected only text and one attachment!")
}
}
func TestEmailTextHtmlAttachment(t *testing.T) {
e := prepareEmail()
e.Text = []byte("Text Body is, of course, supported!\n")
e.HTML = []byte("<h1>Fancy Html is supported, too!</h1>\n")
_, err := e.Attach(bytes.NewBufferString("Rad attachment"), "rad.txt", "text/plain; charset=utf-8")
if err != nil {
t.Fatal("Could not add an attachment to the message: ", err)
}
msg := basicTests(t, e)
// Were the right headers set?
ct := msg.Header.Get("Content-type")
mt, params, err := mime.ParseMediaType(ct)
if err != nil {
t.Fatal("Content-type header is invalid: ", ct)
} else if mt != "multipart/mixed" {
t.Fatalf("Content-type expected \"multipart/mixed\", not %v", mt)
}
b := params["boundary"]
if b == "" {
t.Fatal("Unexpected empty boundary parameter")
}
if len(params) != 1 {
t.Fatal("Unexpected content-type parameters")
}
// Is the generated message parsable?
mixed := multipart.NewReader(msg.Body, params["boundary"])
text, err := mixed.NextPart()
if err != nil {
t.Fatalf("Could not find text component of email: %s", err)
}
// Does the text portion match what we expect?
mt, params, err = mime.ParseMediaType(text.Header.Get("Content-type"))
if err != nil {
t.Fatal("Could not parse message's Content-Type")
} else if mt != "multipart/alternative" {
t.Fatal("Message missing multipart/alternative")
}
mpReader := multipart.NewReader(text, params["boundary"])
part, err := mpReader.NextPart()
if err != nil {
t.Fatal("Could not read plain text component of message: ", err)
}
plainText, err := ioutil.ReadAll(part)
if err != nil {
t.Fatal("Could not read plain text component of message: ", err)
}
if !bytes.Equal(plainText, []byte("Text Body is, of course, supported!\r\n")) {
t.Fatalf("Plain text is broken: %#q", plainText)
}
// Check attachments.
_, err = mixed.NextPart()
if err != nil {
t.Fatalf("Could not find attachment component of email: %s", err)
}
if _, err = mixed.NextPart(); err != io.EOF {
t.Error("Expected only text and one attachment!")
}
}
func TestEmailAttachment(t *testing.T) {
e := prepareEmail()
_, err := e.Attach(bytes.NewBufferString("Rad attachment"), "rad.txt", "text/plain; charset=utf-8")
if err != nil {
t.Fatal("Could not add an attachment to the message: ", err)
}
msg := basicTests(t, e)
// Were the right headers set?
ct := msg.Header.Get("Content-type")
mt, params, err := mime.ParseMediaType(ct)
if err != nil {
t.Fatal("Content-type header is invalid: ", ct)
} else if mt != "multipart/mixed" {
t.Fatalf("Content-type expected \"multipart/mixed\", not %v", mt)
}
b := params["boundary"]
if b == "" {
t.Fatal("Unexpected empty boundary parameter")
}
if len(params) != 1 {
t.Fatal("Unexpected content-type parameters")
}
// Is the generated message parsable?
mixed := multipart.NewReader(msg.Body, params["boundary"])
// Check attachments.
a, err := mixed.NextPart()
if err != nil {
t.Fatalf("Could not find attachment component of email: %s", err)
}
if !strings.HasPrefix(a.Header.Get("Content-Disposition"), "attachment") {
t.Fatalf("Content disposition is not attachment: %s", a.Header.Get("Content-Disposition"))
}
if _, err = mixed.NextPart(); err != io.EOF {
t.Error("Expected only one attachment!")
}
}
func TestHeaderEncoding(t *testing.T) {
cases := []struct {
field string
have string
want string
}{
{
field: "From",
have: "Needs Encóding <encoding@example.com>, Only ASCII <foo@example.com>",
want: "=?utf-8?q?Needs_Enc=C3=B3ding?= <encoding@example.com>, \"Only ASCII\" <foo@example.com>\r\n",
},
{
field: "To",
have: "Keith Moore <moore@cs.utk.edu>, Keld Jørn Simonsen <keld@dkuug.dk>",
want: "\"Keith Moore\" <moore@cs.utk.edu>, =?utf-8?q?Keld_J=C3=B8rn_Simonsen?= <keld@dkuug.dk>\r\n",
},
{
field: "Cc",
have: "Needs Encóding <encoding@example.com>, \"Test :)\" <test@localhost>",
want: "=?utf-8?q?Needs_Enc=C3=B3ding?= <encoding@example.com>, \"Test :)\" <test@localhost>\r\n",
},
{
field: "Subject",
have: "Subject with a 🐟",
want: "=?UTF-8?q?Subject_with_a_=F0=9F=90=9F?=\r\n",
},
{
field: "Subject",
have: "Subject with only ASCII",
want: "Subject with only ASCII\r\n",
},
}
buff := &bytes.Buffer{}
for _, c := range cases {
header := make(textproto.MIMEHeader)
header.Add(c.field, c.have)
buff.Reset()
headerToBytes(buff, header)
want := fmt.Sprintf("%s: %s", c.field, c.want)
got := buff.String()
if got != want {
t.Errorf("invalid utf-8 header encoding. \nwant:%#v\ngot: %#v", want, got)
}
}
}
func TestEmailFromReader(t *testing.T) {
ex := &Email{
Subject: "Test Subject",
To: []string{"Jordan Wright <jmwright798@gmail.com>", "also@example.com"},
From: "Jordan Wright <jmwright798@gmail.com>",
ReplyTo: []string{"Jordan Wright <jmwright798@gmail.com>"},
Cc: []string{"one@example.com", "Two <two@example.com>"},
Bcc: []string{"three@example.com", "Four <four@example.com>"},
Text: []byte("This is a test email with HTML Formatting. It also has very long lines so\nthat the content must be wrapped if using quoted-printable decoding.\n"),
HTML: []byte("<div dir=\"ltr\">This is a test email with <b>HTML Formatting.</b>\u00a0It also has very long lines so that the content must be wrapped if using quoted-printable decoding.</div>\n"),
}
raw := []byte(`
MIME-Version: 1.0
Subject: Test Subject
From: Jordan Wright <jmwright798@gmail.com>
Reply-To: Jordan Wright <jmwright798@gmail.com>
To: Jordan Wright <jmwright798@gmail.com>, also@example.com
Cc: one@example.com, Two <two@example.com>
Bcc: three@example.com, Four <four@example.com>
Content-Type: multipart/alternative; boundary=001a114fb3fc42fd6b051f834280
--001a114fb3fc42fd6b051f834280
Content-Type: text/plain; charset=UTF-8
This is a test email with HTML Formatting. It also has very long lines so
that the content must be wrapped if using quoted-printable decoding.
--001a114fb3fc42fd6b051f834280
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: quoted-printable
<div dir=3D"ltr">This is a test email with <b>HTML Formatting.</b>=C2=A0It =
also has very long lines so that the content must be wrapped if using quote=
d-printable decoding.</div>
--001a114fb3fc42fd6b051f834280--`)
e, err := NewEmailFromReader(bytes.NewReader(raw))
if err != nil {
t.Fatalf("Error creating email %s", err.Error())
}
if e.Subject != ex.Subject {
t.Fatalf("Incorrect subject. %#q != %#q", e.Subject, ex.Subject)
}
if !bytes.Equal(e.Text, ex.Text) {
t.Fatalf("Incorrect text: %#q != %#q", e.Text, ex.Text)
}
if !bytes.Equal(e.HTML, ex.HTML) {
t.Fatalf("Incorrect HTML: %#q != %#q", e.HTML, ex.HTML)
}
if e.From != ex.From {
t.Fatalf("Incorrect \"From\": %#q != %#q", e.From, ex.From)
}
if len(e.To) != len(ex.To) {
t.Fatalf("Incorrect number of \"To\" addresses: %v != %v", len(e.To), len(ex.To))
}
if e.To[0] != ex.To[0] {
t.Fatalf("Incorrect \"To[0]\": %#q != %#q", e.To[0], ex.To[0])
}
if e.To[1] != ex.To[1] {
t.Fatalf("Incorrect \"To[1]\": %#q != %#q", e.To[1], ex.To[1])
}
if len(e.Cc) != len(ex.Cc) {
t.Fatalf("Incorrect number of \"Cc\" addresses: %v != %v", len(e.Cc), len(ex.Cc))
}
if e.Cc[0] != ex.Cc[0] {
t.Fatalf("Incorrect \"Cc[0]\": %#q != %#q", e.Cc[0], ex.Cc[0])
}
if e.Cc[1] != ex.Cc[1] {
t.Fatalf("Incorrect \"Cc[1]\": %#q != %#q", e.Cc[1], ex.Cc[1])
}
if len(e.Bcc) != len(ex.Bcc) {
t.Fatalf("Incorrect number of \"Bcc\" addresses: %v != %v", len(e.Bcc), len(ex.Bcc))
}
if e.Bcc[0] != ex.Bcc[0] {
t.Fatalf("Incorrect \"Bcc[0]\": %#q != %#q", e.Cc[0], ex.Cc[0])
}
if e.Bcc[1] != ex.Bcc[1] {
t.Fatalf("Incorrect \"Bcc[1]\": %#q != %#q", e.Bcc[1], ex.Bcc[1])
}
if len(e.ReplyTo) != len(ex.ReplyTo) {
t.Fatalf("Incorrect number of \"Reply-To\" addresses: %v != %v", len(e.ReplyTo), len(ex.ReplyTo))
}
if e.ReplyTo[0] != ex.ReplyTo[0] {
t.Fatalf("Incorrect \"ReplyTo\": %#q != %#q", e.ReplyTo[0], ex.ReplyTo[0])
}
}
func TestNonAsciiEmailFromReader(t *testing.T) {
ex := &Email{
Subject: "Test Subject",
To: []string{"Anaïs <anais@example.org>"},
Cc: []string{"Patrik Fältström <paf@example.com>"},
From: "Mrs Valérie Dupont <valerie.dupont@example.com>",
Text: []byte("This is a test message!"),
}
raw := []byte(`
MIME-Version: 1.0
Subject: =?UTF-8?Q?Test Subject?=
From: Mrs =?ISO-8859-1?Q?Val=C3=A9rie=20Dupont?= <valerie.dupont@example.com>
To: =?utf-8?q?Ana=C3=AFs?= <anais@example.org>
Cc: =?ISO-8859-1?Q?Patrik_F=E4ltstr=F6m?= <paf@example.com>
Content-type: text/plain; charset=ISO-8859-1
This is a test message!`)
e, err := NewEmailFromReader(bytes.NewReader(raw))
if err != nil {
t.Fatalf("Error creating email %s", err.Error())
}
if e.Subject != ex.Subject {
t.Fatalf("Incorrect subject. %#q != %#q", e.Subject, ex.Subject)
}
if e.From != ex.From {
t.Fatalf("Incorrect \"From\": %#q != %#q", e.From, ex.From)
}
if e.To[0] != ex.To[0] {
t.Fatalf("Incorrect \"To\": %#q != %#q", e.To, ex.To)
}
if e.Cc[0] != ex.Cc[0] {
t.Fatalf("Incorrect \"Cc\": %#q != %#q", e.Cc, ex.Cc)
}
}
func TestNonMultipartEmailFromReader(t *testing.T) {
ex := &Email{
Text: []byte("This is a test message!"),
Subject: "Example Subject (no MIME Type)",
Headers: textproto.MIMEHeader{},
}
ex.Headers.Add("Content-Type", "text/plain; charset=us-ascii")
ex.Headers.Add("Message-ID", "<foobar@example.com>")
raw := []byte(`From: "Foo Bar" <foobar@example.com>
Content-Type: text/plain
To: foobar@example.com
Subject: Example Subject (no MIME Type)
Message-ID: <foobar@example.com>
This is a test message!`)
e, err := NewEmailFromReader(bytes.NewReader(raw))
if err != nil {
t.Fatalf("Error creating email %s", err.Error())
}
if ex.Subject != e.Subject {
t.Errorf("Incorrect subject. %#q != %#q\n", ex.Subject, e.Subject)
}
if !bytes.Equal(ex.Text, e.Text) {
t.Errorf("Incorrect body. %#q != %#q\n", ex.Text, e.Text)
}
if ex.Headers.Get("Message-ID") != e.Headers.Get("Message-ID") {
t.Errorf("Incorrect message ID. %#q != %#q\n", ex.Headers.Get("Message-ID"), e.Headers.Get("Message-ID"))
}
}
func TestBase64EmailFromReader(t *testing.T) {
ex := &Email{
Subject: "Test Subject",
To: []string{"Jordan Wright <jmwright798@gmail.com>"},
From: "Jordan Wright <jmwright798@gmail.com>",
Text: []byte("This is a test email with HTML Formatting. It also has very long lines so that the content must be wrapped if using quoted-printable decoding."),
HTML: []byte("<div dir=\"ltr\">This is a test email with <b>HTML Formatting.</b>\u00a0It also has very long lines so that the content must be wrapped if using quoted-printable decoding.</div>\n"),
}
raw := []byte(`
MIME-Version: 1.0
Subject: Test Subject
From: Jordan Wright <jmwright798@gmail.com>
To: Jordan Wright <jmwright798@gmail.com>
Content-Type: multipart/alternative; boundary=001a114fb3fc42fd6b051f834280
--001a114fb3fc42fd6b051f834280
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: base64
VGhpcyBpcyBhIHRlc3QgZW1haWwgd2l0aCBIVE1MIEZvcm1hdHRpbmcuIEl0IGFsc28gaGFzIHZl
cnkgbG9uZyBsaW5lcyBzbyB0aGF0IHRoZSBjb250ZW50IG11c3QgYmUgd3JhcHBlZCBpZiB1c2lu
ZyBxdW90ZWQtcHJpbnRhYmxlIGRlY29kaW5nLg==
--001a114fb3fc42fd6b051f834280
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: quoted-printable
<div dir=3D"ltr">This is a test email with <b>HTML Formatting.</b>=C2=A0It =
also has very long lines so that the content must be wrapped if using quote=
d-printable decoding.</div>
--001a114fb3fc42fd6b051f834280--`)
e, err := NewEmailFromReader(bytes.NewReader(raw))
if err != nil {
t.Fatalf("Error creating email %s", err.Error())
}
if e.Subject != ex.Subject {
t.Fatalf("Incorrect subject. %#q != %#q", e.Subject, ex.Subject)
}
if !bytes.Equal(e.Text, ex.Text) {
t.Fatalf("Incorrect text: %#q != %#q", e.Text, ex.Text)
}
if !bytes.Equal(e.HTML, ex.HTML) {
t.Fatalf("Incorrect HTML: %#q != %#q", e.HTML, ex.HTML)
}
if e.From != ex.From {
t.Fatalf("Incorrect \"From\": %#q != %#q", e.From, ex.From)
}
}
func TestAttachmentEmailFromReader(t *testing.T) {
ex := &Email{
Subject: "Test Subject",
To: []string{"Jordan Wright <jmwright798@gmail.com>"},
From: "Jordan Wright <jmwright798@gmail.com>",
Text: []byte("Simple text body"),
HTML: []byte("<div dir=\"ltr\">Simple HTML body</div>\n"),
}
a, err := ex.Attach(bytes.NewReader([]byte("Let's just pretend this is raw JPEG data.")), "cat.jpeg", "image/jpeg")
if err != nil {
t.Fatalf("Error attaching image %s", err.Error())
}
b, err := ex.Attach(bytes.NewReader([]byte("Let's just pretend this is raw JPEG data.")), "cat-inline.jpeg", "image/jpeg")
if err != nil {
t.Fatalf("Error attaching inline image %s", err.Error())
}
raw := []byte(`
From: Jordan Wright <jmwright798@gmail.com>
Date: Thu, 17 Oct 2019 08:55:37 +0100
Mime-Version: 1.0
Content-Type: multipart/mixed;
boundary=35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7
To: Jordan Wright <jmwright798@gmail.com>
Subject: Test Subject
--35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7
Content-Type: multipart/alternative;
boundary=b10ca5b1072908cceb667e8968d3af04503b7ab07d61c9f579c15b416d7c
--b10ca5b1072908cceb667e8968d3af04503b7ab07d61c9f579c15b416d7c
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; charset=UTF-8
Simple text body
--b10ca5b1072908cceb667e8968d3af04503b7ab07d61c9f579c15b416d7c
Content-Transfer-Encoding: quoted-printable
Content-Type: text/html; charset=UTF-8
<div dir=3D"ltr">Simple HTML body</div>
--b10ca5b1072908cceb667e8968d3af04503b7ab07d61c9f579c15b416d7c--
--35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7
Content-Disposition: attachment;
filename="cat.jpeg"
Content-Id: <cat.jpeg>
Content-Transfer-Encoding: base64
Content-Type: image/jpeg
TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4=
--35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7
Content-Disposition: inline;
filename="cat-inline.jpeg"
Content-Id: <cat-inline.jpeg>
Content-Transfer-Encoding: base64
Content-Type: image/jpeg
TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4=
--35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7--`)
e, err := NewEmailFromReader(bytes.NewReader(raw))
if err != nil {
t.Fatalf("Error creating email %s", err.Error())
}
if e.Subject != ex.Subject {
t.Fatalf("Incorrect subject. %#q != %#q", e.Subject, ex.Subject)
}
if !bytes.Equal(e.Text, ex.Text) {
t.Fatalf("Incorrect text: %#q != %#q", e.Text, ex.Text)
}
if !bytes.Equal(e.HTML, ex.HTML) {
t.Fatalf("Incorrect HTML: %#q != %#q", e.HTML, ex.HTML)
}
if e.From != ex.From {
t.Fatalf("Incorrect \"From\": %#q != %#q", e.From, ex.From)
}
if len(e.Attachments) != 2 {
t.Fatalf("Incorrect number of attachments %d != %d", len(e.Attachments), 1)
}
if e.Attachments[0].Filename != a.Filename {
t.Fatalf("Incorrect attachment filename %s != %s", e.Attachments[0].Filename, a.Filename)
}
if !bytes.Equal(e.Attachments[0].Content, a.Content) {
t.Fatalf("Incorrect attachment content %#q != %#q", e.Attachments[0].Content, a.Content)
}
if e.Attachments[1].Filename != b.Filename {
t.Fatalf("Incorrect attachment filename %s != %s", e.Attachments[1].Filename, b.Filename)
}
if !bytes.Equal(e.Attachments[1].Content, b.Content) {
t.Fatalf("Incorrect attachment content %#q != %#q", e.Attachments[1].Content, b.Content)
}
}
func ExampleGmail() {
e := NewEmail()
e.From = "Jordan Wright <test@gmail.com>"
e.To = []string{"test@example.com"}
e.Bcc = []string{"test_bcc@example.com"}
e.Cc = []string{"test_cc@example.com"}
e.Subject = "Awesome Subject"
e.Text = []byte("Text Body is, of course, supported!\n")
e.HTML = []byte("<h1>Fancy Html is supported, too!</h1>\n")
e.Send("smtp.gmail.com:587", smtp.PlainAuth("", e.From, "password123", "smtp.gmail.com"))
}
func ExampleAttach() {
e := NewEmail()
e.AttachFile("test.txt")
}
func Test_base64Wrap(t *testing.T) {
file := "I'm a file long enough to force the function to wrap a\n" +
"couple of lines, but I stop short of the end of one line and\n" +
"have some padding dangling at the end."
encoded := "SSdtIGEgZmlsZSBsb25nIGVub3VnaCB0byBmb3JjZSB0aGUgZnVuY3Rpb24gdG8gd3JhcCBhCmNv\r\n" +
"dXBsZSBvZiBsaW5lcywgYnV0IEkgc3RvcCBzaG9ydCBvZiB0aGUgZW5kIG9mIG9uZSBsaW5lIGFu\r\n" +
"ZApoYXZlIHNvbWUgcGFkZGluZyBkYW5nbGluZyBhdCB0aGUgZW5kLg==\r\n"
var buf bytes.Buffer
base64Wrap(&buf, []byte(file))
if !bytes.Equal(buf.Bytes(), []byte(encoded)) {
t.Fatalf("Encoded file does not match expected: %#q != %#q", string(buf.Bytes()), encoded)
}
}
// *Since the mime library in use by ```email``` is now in the stdlib, this test is deprecated
func Test_quotedPrintEncode(t *testing.T) {
var buf bytes.Buffer
text := []byte("Dear reader!\n\n" +
"This is a test email to try and capture some of the corner cases that exist within\n" +
"the quoted-printable encoding.\n" +
"There are some wacky parts like =, and this input assumes UNIX line breaks so\r\n" +
"it can come out a little weird. Also, we need to support unicode so here's a fish: 🐟\n")
expected := []byte("Dear reader!\r\n\r\n" +
"This is a test email to try and capture some of the corner cases that exist=\r\n" +
" within\r\n" +
"the quoted-printable encoding.\r\n" +
"There are some wacky parts like =3D, and this input assumes UNIX line break=\r\n" +
"s so\r\n" +
"it can come out a little weird. Also, we need to support unicode so here's=\r\n" +
" a fish: =F0=9F=90=9F\r\n")
qp := quotedprintable.NewWriter(&buf)
if _, err := qp.Write(text); err != nil {
t.Fatal("quotePrintEncode: ", err)
}
if err := qp.Close(); err != nil {
t.Fatal("Error closing writer", err)
}
if b := buf.Bytes(); !bytes.Equal(b, expected) {
t.Errorf("quotedPrintEncode generated incorrect results: %#q != %#q", b, expected)
}
}
func TestMultipartNoContentType(t *testing.T) {
raw := []byte(`From: Mikhail Gusarov <dottedmag@dottedmag.net>
To: notmuch@notmuchmail.org
References: <20091117190054.GU3165@dottiness.seas.harvard.edu>
Date: Wed, 18 Nov 2009 01:02:38 +0600
Message-ID: <87iqd9rn3l.fsf@vertex.dottedmag>
MIME-Version: 1.0
Subject: Re: [notmuch] Working with Maildir storage?
Content-Type: multipart/mixed; boundary="===============1958295626=="
--===============1958295626==
Content-Type: multipart/signed; boundary="=-=-=";
micalg=pgp-sha1; protocol="application/pgp-signature"
--=-=-=
Content-Transfer-Encoding: quoted-printable
Twas brillig at 14:00:54 17.11.2009 UTC-05 when lars@seas.harvard.edu did g=
yre and gimble:
--=-=-=
Content-Type: application/pgp-signature
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (GNU/Linux)
iQIcBAEBAgAGBQJLAvNOAAoJEJ0g9lA+M4iIjLYQAKp0PXEgl3JMOEBisH52AsIK
=/ksP
-----END PGP SIGNATURE-----
--=-=-=--
--===============1958295626==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: inline
Testing!
--===============1958295626==--
`)
e, err := NewEmailFromReader(bytes.NewReader(raw))
if err != nil {
t.Fatalf("Error when parsing email %s", err.Error())
}
if !bytes.Equal(e.Text, []byte("Testing!")) {
t.Fatalf("Error incorrect text: %#q != %#q\n", e.Text, "Testing!")
}
}
func TestNoMultipartHTMLContentTypeBase64Encoding(t *testing.T) {
raw := []byte(`MIME-Version: 1.0
From: no-reply@example.com
To: tester@example.org
Date: 7 Jan 2021 03:07:44 -0800
Subject: Hello
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: base64
Message-Id: <20210107110744.547DD70532@example.com>
PGh0bWw+PGhlYWQ+PHRpdGxlPnRlc3Q8L3RpdGxlPjwvaGVhZD48Ym9keT5IZWxsbyB3
b3JsZCE8L2JvZHk+PC9odG1sPg==
`)
e, err := NewEmailFromReader(bytes.NewReader(raw))
if err != nil {
t.Fatalf("Error when parsing email %s", err.Error())
}
if !bytes.Equal(e.HTML, []byte("<html><head><title>test</title></head><body>Hello world!</body></html>")) {
t.Fatalf("Error incorrect text: %#q != %#q\n", e.Text, "<html>...</html>")
}
}
// *Since the mime library in use by ```email``` is now in the stdlib, this test is deprecated
func Test_quotedPrintDecode(t *testing.T) {
text := []byte("Dear reader!\r\n\r\n" +
"This is a test email to try and capture some of the corner cases that exist=\r\n" +
" within\r\n" +
"the quoted-printable encoding.\r\n" +
"There are some wacky parts like =3D, and this input assumes UNIX line break=\r\n" +
"s so\r\n" +
"it can come out a little weird. Also, we need to support unicode so here's=\r\n" +
" a fish: =F0=9F=90=9F\r\n")
expected := []byte("Dear reader!\r\n\r\n" +
"This is a test email to try and capture some of the corner cases that exist within\r\n" +
"the quoted-printable encoding.\r\n" +
"There are some wacky parts like =, and this input assumes UNIX line breaks so\r\n" +
"it can come out a little weird. Also, we need to support unicode so here's a fish: 🐟\r\n")
qp := quotedprintable.NewReader(bytes.NewReader(text))
got, err := ioutil.ReadAll(qp)
if err != nil {
t.Fatal("quotePrintDecode: ", err)
}
if !bytes.Equal(got, expected) {
t.Errorf("quotedPrintDecode generated incorrect results: %#q != %#q", got, expected)
}
}
func Benchmark_base64Wrap(b *testing.B) {
// Reasonable base case; 128K random bytes
file := make([]byte, 128*1024)
if _, err := rand.Read(file); err != nil {
panic(err)
}
for i := 0; i <= b.N; i++ {
base64Wrap(ioutil.Discard, file)
}
}
func TestParseSender(t *testing.T) {
var cases = []struct {
e Email
want string
haserr bool
}{
{
Email{From: "from@test.com"},
"from@test.com",
false,
},
{
Email{Sender: "sender@test.com", From: "from@test.com"},
"sender@test.com",
false,
},
{
Email{Sender: "bad_address_sender"},
"",
true,
},
{
Email{Sender: "good@sender.com", From: "bad_address_from"},
"good@sender.com",
false,
},
}
for i, testcase := range cases {
got, err := testcase.e.parseSender()
if got != testcase.want || (err != nil) != testcase.haserr {
t.Errorf(`%d: got %s != want %s or error "%t" != "%t"`, i+1, got, testcase.want, err != nil, testcase.haserr)
}
}
}

@ -0,0 +1,367 @@
package email
import (
"crypto/tls"
"errors"
"io"
"net"
"net/mail"
"net/smtp"
"net/textproto"
"sync"
"syscall"
"time"
)
type Pool struct {
addr string
auth smtp.Auth
max int
created int
clients chan *client
rebuild chan struct{}
mut *sync.Mutex
lastBuildErr *timestampedErr
closing chan struct{}
tlsConfig *tls.Config
helloHostname string
}
type client struct {
*smtp.Client
failCount int
}
type timestampedErr struct {
err error
ts time.Time
}
const maxFails = 4
var (
ErrClosed = errors.New("pool closed")
ErrTimeout = errors.New("timed out")
)
func NewPool(address string, count int, auth smtp.Auth, opt_tlsConfig ...*tls.Config) (pool *Pool, err error) {
pool = &Pool{
addr: address,
auth: auth,
max: count,
clients: make(chan *client, count),
rebuild: make(chan struct{}),
closing: make(chan struct{}),
mut: &sync.Mutex{},
}
if len(opt_tlsConfig) == 1 {
pool.tlsConfig = opt_tlsConfig[0]
} else if host, _, e := net.SplitHostPort(address); e != nil {
return nil, e
} else {
pool.tlsConfig = &tls.Config{ServerName: host}
}
return
}
// go1.1 didn't have this method
func (c *client) Close() error {
return c.Text.Close()
}
// SetHelloHostname optionally sets the hostname that the Go smtp.Client will
// use when doing a HELLO with the upstream SMTP server. By default, Go uses
// "localhost" which may not be accepted by certain SMTP servers that demand
// an FQDN.
func (p *Pool) SetHelloHostname(h string) {
p.helloHostname = h
}
func (p *Pool) get(timeout time.Duration) *client {
select {
case c := <-p.clients:
return c
default:
}
if p.created < p.max {
p.makeOne()
}
var deadline <-chan time.Time
if timeout >= 0 {
deadline = time.After(timeout)
}
for {
select {
case c := <-p.clients:
return c
case <-p.rebuild:
p.makeOne()
case <-deadline:
return nil
case <-p.closing:
return nil
}
}
}
func shouldReuse(err error) bool {
// certainly not perfect, but might be close:
// - EOF: clearly, the connection went down
// - textproto.Errors were valid SMTP over a valid connection,
// but resulted from an SMTP error response
// - textproto.ProtocolErrors result from connections going down,
// invalid SMTP, that sort of thing
// - syscall.Errno is probably down connection/bad pipe, but
// passed straight through by textproto instead of becoming a
// ProtocolError
// - if we don't recognize the error, don't reuse the connection
// A false positive will probably fail on the Reset(), and even if
// not will eventually hit maxFails.
// A false negative will knock over (and trigger replacement of) a
// conn that might have still worked.
if err == io.EOF {
return false
}
switch err.(type) {
case *textproto.Error:
return true
case *textproto.ProtocolError, textproto.ProtocolError:
return false
case syscall.Errno:
return false
default:
return false
}
}
func (p *Pool) replace(c *client) {
p.clients <- c
}
func (p *Pool) inc() bool {
if p.created >= p.max {
return false
}
p.mut.Lock()
defer p.mut.Unlock()
if p.created >= p.max {
return false
}
p.created++
return true
}
func (p *Pool) dec() {
p.mut.Lock()
p.created--
p.mut.Unlock()
select {
case p.rebuild <- struct{}{}:
default:
}
}
func (p *Pool) makeOne() {
go func() {
if p.inc() {
if c, err := p.build(); err == nil {
p.clients <- c
} else {
p.lastBuildErr = &timestampedErr{err, time.Now()}
p.dec()
}
}
}()
}
func startTLS(c *client, t *tls.Config) (bool, error) {
if ok, _ := c.Extension("STARTTLS"); !ok {
return false, nil
}
if err := c.StartTLS(t); err != nil {
return false, err
}
return true, nil
}
func addAuth(c *client, auth smtp.Auth) (bool, error) {
if ok, _ := c.Extension("AUTH"); !ok {
return false, nil
}
if err := c.Auth(auth); err != nil {
return false, err
}
return true, nil
}
func (p *Pool) build() (*client, error) {
cl, err := smtp.Dial(p.addr)
if err != nil {
return nil, err
}
// Is there a custom hostname for doing a HELLO with the SMTP server?
if p.helloHostname != "" {
cl.Hello(p.helloHostname)
}
c := &client{cl, 0}
if _, err := startTLS(c, p.tlsConfig); err != nil {
c.Close()
return nil, err
}
if p.auth != nil {
if _, err := addAuth(c, p.auth); err != nil {
c.Close()
return nil, err
}
}
return c, nil
}
func (p *Pool) maybeReplace(err error, c *client) {
if err == nil {
c.failCount = 0
p.replace(c)
return
}
c.failCount++
if c.failCount >= maxFails {
goto shutdown
}
if !shouldReuse(err) {
goto shutdown
}
if err := c.Reset(); err != nil {
goto shutdown
}
p.replace(c)
return
shutdown:
p.dec()
c.Close()
}
func (p *Pool) failedToGet(startTime time.Time) error {
select {
case <-p.closing:
return ErrClosed
default:
}
if p.lastBuildErr != nil && startTime.Before(p.lastBuildErr.ts) {
return p.lastBuildErr.err
}
return ErrTimeout
}
// Send sends an email via a connection pulled from the Pool. The timeout may
// be <0 to indicate no timeout. Otherwise reaching the timeout will produce
// and error building a connection that occurred while we were waiting, or
// otherwise ErrTimeout.
func (p *Pool) Send(e *Email, timeout time.Duration) (err error) {
start := time.Now()
c := p.get(timeout)
if c == nil {
return p.failedToGet(start)
}
defer func() {
p.maybeReplace(err, c)
}()
recipients, err := addressLists(e.To, e.Cc, e.Bcc)
if err != nil {
return
}
msg, err := e.Bytes()
if err != nil {
return
}
from, err := emailOnly(e.From)
if err != nil {
return
}
if err = c.Mail(from); err != nil {
return
}
for _, recip := range recipients {
if err = c.Rcpt(recip); err != nil {
return
}
}
w, err := c.Data()
if err != nil {
return
}
if _, err = w.Write(msg); err != nil {
return
}
err = w.Close()
return
}
func emailOnly(full string) (string, error) {
addr, err := mail.ParseAddress(full)
if err != nil {
return "", err
}
return addr.Address, nil
}
func addressLists(lists ...[]string) ([]string, error) {
length := 0
for _, lst := range lists {
length += len(lst)
}
combined := make([]string, 0, length)
for _, lst := range lists {
for _, full := range lst {
addr, err := emailOnly(full)
if err != nil {
return nil, err
}
combined = append(combined, addr)
}
}
return combined, nil
}
// Close immediately changes the pool's state so no new connections will be
// created, then gets and closes the existing ones as they become available.
func (p *Pool) Close() {
close(p.closing)
for p.created > 0 {
c := <-p.clients
c.Quit()
p.dec()
}
}

@ -0,0 +1,132 @@
package smtpclient
import (
"b612.me/apps/b612/smtpclient/email"
"b612.me/stario"
"b612.me/starlog"
"crypto/tls"
"github.com/spf13/cobra"
"mime"
"net/smtp"
"os"
"path/filepath"
"strings"
)
var Cmd = &cobra.Command{
Use: "smtpc",
Short: "smtp client",
Long: "smtp client",
Run: func(cmd *cobra.Command, args []string) {
run()
},
}
var from, subject, text string
var to, cc, bcc, attachments, replyTo []string
var useHTML bool
var user, pwd, server string
var skipInsecure, usingFile bool
var useTLS int
func init() {
Cmd.Flags().BoolVarP(&usingFile, "file", "F", false, "using file")
Cmd.Flags().StringSliceVarP(&replyTo, "reply-to", "r", nil, "reply to")
Cmd.Flags().StringVarP(&from, "from", "f", "", "from")
Cmd.Flags().StringVarP(&subject, "subject", "s", "", "subject")
Cmd.Flags().StringVarP(&text, "text", "t", "", "text")
Cmd.Flags().StringSliceVarP(&to, "to", "T", nil, "to")
Cmd.Flags().StringSliceVarP(&cc, "cc", "C", nil, "cc")
Cmd.Flags().StringSliceVarP(&bcc, "bcc", "B", nil, "bcc")
Cmd.Flags().StringSliceVarP(&attachments, "attachments", "a", nil, "attachments")
Cmd.Flags().BoolVarP(&useHTML, "html", "H", false, "use html")
Cmd.Flags().StringVarP(&user, "user", "u", "", "user")
Cmd.Flags().StringVarP(&pwd, "pwd", "p", "", "password")
Cmd.Flags().StringVarP(&server, "server", "S", "127.0.0.1:25", "server")
Cmd.Flags().IntVarP(&useTLS, "tls", "l", 0, "use tls,1 means use tls,2 means use starttls,other means not use tls")
Cmd.Flags().BoolVarP(&skipInsecure, "skip-insecure", "i", false, "skip insecure")
}
func run() {
{
for from == "" {
from = stario.MessageBox("Please input mail from:", "").MustString()
}
for len(to) == 0 {
to = stario.MessageBox("Please input mail to,split by ,:", "").MustSliceString(",")
}
for subject == "" {
subject = stario.MessageBox("Please input mail subject:", "").MustString()
}
for text == "" {
text = stario.MessageBox("Please input mail text:", "").MustString()
}
for server == "" {
server = stario.MessageBox("Please input mail server:", "").MustString()
}
}
var mail = email.Email{
ReplyTo: replyTo,
From: from,
To: to,
Bcc: bcc,
Cc: cc,
Subject: subject,
}
var txt []byte
var err error
if usingFile {
txt, err = os.ReadFile(text)
if err != nil {
starlog.Errorf("read file %s error:%s\n", text, err)
os.Exit(1)
}
} else {
txt = []byte(text)
}
if useHTML {
mail.HTML = txt
} else {
mail.Text = txt
}
if len(attachments) > 0 {
for _, v := range attachments {
if strings.TrimSpace(v) == "" {
continue
}
f, err := os.Open(v)
if err != nil {
starlog.Errorf("open attach file %s error:%s\n", v, err)
os.Exit(1)
}
stat, err := f.Stat()
if err != nil {
starlog.Errorf("stat attach file %s error:%s\n", v, err)
os.Exit(1)
}
_, err = mail.Attach(f, stat.Name(), mime.TypeByExtension(filepath.Ext(stat.Name())))
if err != nil {
starlog.Errorf("attach file %s error:%s\n", v, err)
os.Exit(1)
}
}
}
var auth smtp.Auth
if user != "" && pwd != "" {
auth = smtp.PlainAuth("", user, pwd, server)
}
switch useTLS {
case 1:
err = mail.SendWithTLS(server, auth, &tls.Config{InsecureSkipVerify: skipInsecure})
case 2:
err = mail.SendWithStartTLS(server, auth, &tls.Config{InsecureSkipVerify: skipInsecure})
default:
err = mail.Send(server, auth)
}
if err != nil {
starlog.Errorf("send mail error:%s\n", err)
os.Exit(1)
}
starlog.Infof("send mail to %v success by server %s\n", to, server)
}

@ -0,0 +1,13 @@
package smtpserver
import (
"fmt"
"mime"
"testing"
)
func TestSession_Data(t *testing.T) {
s := `=?UTF-8?q?=E5=85=B7=E8=B6=B3=E8=99=AB=E7=94=B5=E8=B4=BA=E8=A6=81=E8=BE=8A?= =?UTF-8?q?=E6=A5=9Ejhkkj?=`
d := new(mime.WordDecoder)
fmt.Println(d.DecodeHeader(s))
}

@ -2,14 +2,20 @@ package smtpserver
import (
"b612.me/starlog"
"b612.me/startext"
"bytes"
"flag"
"crypto/tls"
"fmt"
"github.com/spf13/cobra"
"html"
"io"
"io/ioutil"
"mime"
"mime/quotedprintable"
"net/mail"
"os"
"strings"
"time"
"github.com/emersion/go-smtp"
)
@ -19,9 +25,10 @@ var user, pass string
var allowAnyuser bool
var output string
var domain string
var cert, key string
var Cmd = &cobra.Command{
Use: "smtp",
Use: "smtps",
Short: "smtp server",
Long: "smtp server",
Run: func(cmd *cobra.Command, args []string) {
@ -37,6 +44,8 @@ func init() {
Cmd.Flags().BoolVarP(&allowAnyuser, "allow-anyuser", "A", false, "allow any user")
Cmd.Flags().StringVarP(&output, "output", "o", "", "output mail to html")
Cmd.Flags().StringVarP(&domain, "domain", "d", "localhost", "smtp server domain")
Cmd.Flags().StringVarP(&cert, "cert", "c", "", "smtp server cert(TLS)")
Cmd.Flags().StringVarP(&key, "key", "k", "", "smtp server key(TLS)")
}
type backend struct{}
@ -85,24 +94,66 @@ func (s *session) Data(r io.Reader) error {
}
header := msg.Header
subject := header.Get("Subject")
to := header.Get("To")
cc := header.Get("Cc")
bcc := header.Get("Bcc")
from := header.Get("From") // 获取发件人
starlog.Println("From:", from)
starlog.Println("Subject:", subject)
starlog.Println("Cc:", cc)
starlog.Println("Bcc:", bcc)
// Read the body
date := header.Get("Date")
body, err := ioutil.ReadAll(msg.Body)
if err != nil {
return err
}
starlog.Println("Body:", string(body))
var bodyStr string
var d = new(mime.WordDecoder)
{
subject, err = d.DecodeHeader(subject)
if err != nil {
starlog.Errorf("Decode subject %s error:%s\n", subject, err)
return err
}
bodyStr, err = bodyDecode(string(body))
if err != nil {
starlog.Errorf("Decode body %s error:%s\n", string(body), err)
return err
}
}
if startext.IsGBK([]byte(bodyStr)) {
tmp, err := startext.GBK2UTF8([]byte(bodyStr))
if err == nil {
bodyStr = string(tmp)
}
}
starlog.Println("From:", from)
starlog.Println("Subject:", subject)
starlog.Println("To ALL:", s.to)
starlog.Println("To:", to)
starlog.Println("Cc:", cc)
starlog.Println("Body:", bodyStr)
if output != "" {
path := output + "/" + subject + ".html"
html := fmt.Sprintf(`<html><head><title>%s</title></head><body><h2>subject: %s</p><p>mali from: %s</p<p>mail to:%s</p>
<p>cc:%s</p><p>bcc:%s</p><br /><p>%s</p></body></html>`, subject, subject, from, s.to, cc, bcc, string(body))
path := fmt.Sprintf("%s/%s_%s.html", output, subject, time.Now().Format("2006_01_02_15_04_05_"))
html := fmt.Sprintf(`<html><head>
<meta charset="utf-8">
<title>%s</title>
</head>
<body>
<h2>%s</h2>
<hr>
<p>auth user:<strong> %s </strong></p>
<p>auth pass:<strong> %s </strong></p>
<hr>
<p>Date:<strong> %v </strong></p>
<p>From:<strong> %s </strong></p>
<p>To All:<strong> %s </strong></p>
<p>To:<strong> %s </strong></p>
<p>Cc:<strong> %s </strong></p>
<hr>
<br />
<br />
%s
</body>
</html>`, html.EscapeString(subject), html.EscapeString(subject), html.EscapeString(s.username), html.EscapeString(s.password),
date, html.EscapeString(from), html.EscapeString(s.to), html.EscapeString(to), html.EscapeString(cc), bodyStr)
os.WriteFile(path, []byte(html), 0644)
}
@ -116,10 +167,25 @@ func (s *session) Logout() error {
}
func run() {
flag.Parse()
var err error
s := smtp.NewServer(&backend{})
if cert != "" && key != "" {
var config tls.Config
config.Certificates = make([]tls.Certificate, 1)
config.Certificates[0], err = tls.LoadX509KeyPair(cert, key)
if err != nil {
starlog.Errorln("failed to load cert:", err)
return
}
s.TLSConfig = &config
s.Addr = addr
s.Domain = domain
s.AllowInsecureAuth = true
s.Debug = os.Stdout
starlog.Infoln("Starting TLS-SMTP server at", addr)
starlog.Errorln(s.ListenAndServeTLS())
return
}
s.Addr = addr
s.Domain = domain
s.AllowInsecureAuth = true
@ -128,3 +194,19 @@ func run() {
starlog.Infoln("Starting SMTP server at", addr)
starlog.Errorln(s.ListenAndServe())
}
func bodyDecode(encoded string) (string, error) {
encoded = strings.Replace(encoded, "=\r\n", "", -1) // 对于Windows系统
encoded = strings.Replace(encoded, "=\n", "", -1) // 对于UNIX/Linux系统
// 创建一个新的Quoted-Printable阅读器
reader := quotedprintable.NewReader(strings.NewReader(encoded))
// 读取并解码整个内容
decoded, err := ioutil.ReadAll(reader)
if err != nil {
fmt.Println("Error decoding string:", err)
return string(decoded), err
}
return string(decoded), nil
}

@ -7,6 +7,6 @@ import "github.com/spf13/cobra"
var Cmd = &cobra.Command{
Use: "uac",
Short: "run process with administrator permission",
Example: "vtqe uac 'c:\\program.exe arg1 arg2'",
Example: "b612 uac 'c:\\program.exe arg1 arg2'",
Hidden: true,
}

Loading…
Cancel
Save