diff --git a/cert/ca.go b/cert/ca.go
new file mode 100644
index 0000000..f07eeee
--- /dev/null
+++ b/cert/ca.go
@@ -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
+}
diff --git a/cert/cert.go b/cert/cert.go
new file mode 100644
index 0000000..61a5093
--- /dev/null
+++ b/cert/cert.go
@@ -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
+}
diff --git a/cert/cmd.go b/cert/cmd.go
new file mode 100644
index 0000000..c50ab24
--- /dev/null
+++ b/cert/cmd.go
@@ -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")
+ }
+ },
+}
diff --git a/cert/csr.go b/cert/csr.go
new file mode 100644
index 0000000..90c3a6c
--- /dev/null
+++ b/cert/csr.go
@@ -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
+}
diff --git a/go.mod b/go.mod
index 6831b74..bb35849 100644
--- a/go.mod
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
index 3f6b01f..165a993 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
diff --git a/httpreverse/service.go b/httpreverse/service.go
index 0f743ba..70b152b 100644
--- a/httpreverse/service.go
+++ b/httpreverse/service.go
@@ -17,7 +17,7 @@ import (
"time"
)
-var version = "2.0.1"
+var version = "2.1.0"
func (h *ReverseConfig) Run() error {
err := h.init()
diff --git a/httpserver/server.go b/httpserver/server.go
index 440fa79..5e9dfb7 100644
--- a/httpserver/server.go
+++ b/httpserver/server.go
@@ -22,7 +22,7 @@ import (
"time"
)
-var version = "2.0.1"
+var version = "2.1.0"
type HttpServerCfgs func(cfg *HttpServerCfg)
diff --git a/keygen/cmd.go b/keygen/cmd.go
index 769bead..8a1c789 100644
--- a/keygen/cmd.go
+++ b/keygen/cmd.go
@@ -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)
+ },
+}
diff --git a/main.go b/main.go
index a874ebe..a145687 100644
--- a/main.go
+++ b/main.go
@@ -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() {
diff --git a/smtpclient/email/LICENSE b/smtpclient/email/LICENSE
new file mode 100644
index 0000000..678f42d
--- /dev/null
+++ b/smtpclient/email/LICENSE
@@ -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.
diff --git a/smtpclient/email/email.go b/smtpclient/email/email.go
new file mode 100644
index 0000000..57d1b53
--- /dev/null
+++ b/smtpclient/email/email.go
@@ -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
+}
diff --git a/smtpclient/email/email_test.go b/smtpclient/email/email_test.go
new file mode 100644
index 0000000..b6d62d2
--- /dev/null
+++ b/smtpclient/email/email_test.go
@@ -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 Fancy Html is supported, too!
\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("Fancy Html is supported, too!
\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 Fancy Html is supported, too!
\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 subject: %s
mali from: %s
mail to:%s -cc:%s
bcc:%s
%s
`, 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(` + +auth user: %s
+auth pass: %s
+Date: %v
+From: %s
+To All: %s
+To: %s
+Cc: %s
+