2023-06-13 23:31:19 -04:00
package main
import (
2023-08-01 11:28:17 -04:00
"errors"
2023-06-15 09:45:14 -04:00
"fmt"
2023-06-15 10:30:20 -04:00
"io"
2023-06-13 23:31:19 -04:00
"os"
2023-07-12 12:07:31 -04:00
"runtime/debug"
2023-07-31 10:32:02 -04:00
"strconv"
2023-06-15 10:59:25 -04:00
"strings"
2023-06-13 23:31:19 -04:00
tea "github.com/charmbracelet/bubbletea"
2023-07-13 11:05:27 -04:00
mcobra "github.com/muesli/mango-cobra"
"github.com/muesli/roff"
2023-06-15 10:30:20 -04:00
"github.com/resendlabs/resend-go"
2023-06-13 23:31:19 -04:00
"github.com/spf13/cobra"
)
2023-07-31 10:32:02 -04:00
// PopUnsafeHTML is the environment variable that enables unsafe HTML in the
// email body.
const PopUnsafeHTML = "POP_UNSAFE_HTML"
// ResendAPIKey is the environment variable that enables Resend as a delivery
// method and uses it to send the email.
const ResendAPIKey = "RESEND_API_KEY" //nolint:gosec
// PopFrom is the environment variable that sets the default "from" address.
const PopFrom = "POP_FROM"
// PopSignature is the environment variable that sets the default signature.
const PopSignature = "POP_SIGNATURE"
// PopSMTPHost is the host for the SMTP server if the user is using the SMTP delivery method.
const PopSMTPHost = "POP_SMTP_HOST"
// PopSMTPPort is the port for the SMTP server if the user is using the SMTP delivery method.
const PopSMTPPort = "POP_SMTP_PORT"
// PopSMTPUsername is the username for the SMTP server if the user is using the SMTP delivery method.
const PopSMTPUsername = "POP_SMTP_USERNAME"
// PopSMTPPassword is the password for the SMTP server if the user is using the SMTP delivery method.
const PopSMTPPassword = "POP_SMTP_PASSWORD" //nolint:gosec
// PopSMTPEncryption is the encryption type for the SMTP server if the user is using the SMTP delivery method.
const PopSMTPEncryption = "POP_SMTP_ENCRYPTION" //nolint:gosec
// PopSMTPInsecureSkipVerify is whether or not to skip TLS verification for the
// SMTP server if the user is using the SMTP delivery method.
const PopSMTPInsecureSkipVerify = "POP_SMTP_INSECURE_SKIP_VERIFY"
2023-06-15 10:30:20 -04:00
var (
2023-07-31 10:32:02 -04:00
from string
to [ ] string
cc [ ] string
bcc [ ] string
subject string
body string
attachments [ ] string
preview bool
unsafe bool
signature string
smtpHost string
smtpPort int
smtpUsername string
smtpPassword string
smtpEncryption string
smtpInsecureSkipVerify bool
resendAPIKey string
2023-06-15 10:30:20 -04:00
)
2023-06-15 09:45:14 -04:00
2023-06-13 23:31:19 -04:00
var rootCmd = & cobra . Command {
2023-07-12 12:07:31 -04:00
Use : "pop" ,
Short : "Send emails from your terminal" ,
Long : ` Pop is a tool for sending emails from your terminal. ` ,
2023-06-13 23:31:19 -04:00
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2026-04-09 21:48:21 +08:00
if ! configExists ( ) && smtpUsername == "" && smtpPassword == "" && resendAPIKey == "" {
if err := runOnboarding ( ) ; err != nil {
return err
}
}
2023-07-31 10:32:02 -04:00
var deliveryMethod DeliveryMethod
switch {
2023-08-17 12:15:37 -03:00
case resendAPIKey != "" && smtpUsername != "" && smtpPassword != "" :
deliveryMethod = Unknown
2023-07-31 10:32:02 -04:00
case resendAPIKey != "" :
deliveryMethod = Resend
case smtpUsername != "" && smtpPassword != "" :
deliveryMethod = SMTP
2023-09-26 15:53:51 -04:00
if from == "" {
from = smtpUsername
}
2023-07-31 10:32:02 -04:00
}
2023-08-17 12:15:37 -03:00
switch deliveryMethod {
case None :
2026-04-09 21:48:21 +08:00
fmt . Printf ( "\n %s 未找到邮件配置\n\n" , errorHeaderStyle . String ( ) )
fmt . Printf ( " %s 请运行 %s 进行首次配置\n\n" , commentStyle . Render ( "提示:" ) , inlineCodeStyle . Render ( "pop --config" ) )
2023-08-01 11:28:17 -04:00
cmd . SilenceUsage = true
cmd . SilenceErrors = true
2026-04-09 21:48:21 +08:00
return errors . New ( "missing mail configuration" )
2023-08-17 12:15:37 -03:00
case Unknown :
fmt . Printf ( "\n %s Unknown delivery method.\n" , errorHeaderStyle . String ( ) )
fmt . Printf ( "\n You have set both %s and %s delivery methods." , inlineCodeStyle . Render ( ResendAPIKey ) , inlineCodeStyle . Render ( "POP_SMPT_*" ) )
fmt . Printf ( "\n Set only one of these environment variables.\n\n" )
cmd . SilenceUsage = true
cmd . SilenceErrors = true
return errors . New ( "unknown delivery method" )
2023-06-15 15:17:22 -04:00
}
2025-03-25 21:34:34 +01:00
if body == "" && hasStdin ( ) {
2023-06-15 10:30:20 -04:00
b , err := io . ReadAll ( os . Stdin )
if err != nil {
return err
}
body = string ( b )
}
2023-07-11 10:21:28 -04:00
if signature != "" {
body += "\n\n" + signature
}
2023-07-10 12:37:02 -04:00
if len ( to ) > 0 && from != "" && subject != "" && body != "" && ! preview {
2023-07-31 10:32:02 -04:00
var err error
switch deliveryMethod {
case SMTP :
err = sendSMTPEmail ( to , cc , bcc , from , subject , body , attachments )
case Resend :
err = sendResendEmail ( to , cc , bcc , from , subject , body , attachments )
default :
err = fmt . Errorf ( "unknown delivery method" )
}
2023-06-15 10:30:20 -04:00
if err != nil {
2023-06-15 10:52:27 -04:00
cmd . SilenceUsage = true
cmd . SilenceErrors = true
fmt . Println ( errorStyle . Render ( err . Error ( ) ) )
2023-06-15 10:30:20 -04:00
return err
}
2023-06-15 11:03:14 -04:00
fmt . Print ( emailSummary ( to , subject ) )
2023-06-15 10:30:20 -04:00
return nil
}
p := tea . NewProgram ( NewModel ( resend . SendEmailRequest {
2023-06-27 14:34:28 -04:00
From : from ,
To : to ,
2024-04-25 11:18:45 -04:00
Bcc : bcc ,
Cc : cc ,
2023-06-27 14:34:28 -04:00
Subject : subject ,
Text : body ,
Attachments : makeAttachments ( attachments ) ,
2023-07-31 10:32:02 -04:00
} , deliveryMethod ) )
2023-06-15 10:59:25 -04:00
m , err := p . Run ( )
2023-06-13 23:31:19 -04:00
if err != nil {
return err
}
2023-06-15 10:59:25 -04:00
mm := m . ( Model )
2023-06-15 11:04:38 -04:00
if ! mm . abort {
2023-07-31 10:32:02 -04:00
fmt . Print ( emailSummary ( strings . Split ( mm . To . Value ( ) , ToSeparator ) , mm . Subject . Value ( ) ) )
2023-06-15 11:04:38 -04:00
}
2023-06-13 23:31:19 -04:00
return nil
} ,
}
2023-06-15 10:30:20 -04:00
// hasStdin returns whether there is data in stdin.
func hasStdin ( ) bool {
stat , err := os . Stdin . Stat ( )
return err == nil && ( stat . Mode ( ) & os . ModeCharDevice ) == 0
}
2023-07-12 12:07:31 -04:00
var (
// Version stores the build version of VHS at the time of package through
// -ldflags.
//
// go build -ldflags "-s -w -X=main.Version=$(VERSION)"
Version string
// CommitSHA stores the git commit SHA at the time of package through -ldflags.
CommitSHA string
)
2023-07-31 10:32:02 -04:00
// ManCmd is the cobra command for the manual.
2023-07-13 11:05:27 -04:00
var ManCmd = & cobra . Command {
Use : "man" ,
Short : "Generate man page" ,
Long : ` To generate the man page ` ,
Args : cobra . NoArgs ,
Hidden : true ,
2023-08-01 10:43:39 -04:00
RunE : func ( _ * cobra . Command , _ [ ] string ) error {
2023-07-13 11:05:27 -04:00
page , err := mcobra . NewManPage ( 1 , rootCmd ) // .
if err != nil {
return err
}
page = page . WithSection ( "Copyright" , "© 2023 Charmbracelet, Inc.\n" + "Released under MIT License." )
fmt . Println ( page . Build ( roff . NewDocument ( ) ) )
return nil
} ,
}
2023-06-15 10:30:20 -04:00
func init ( ) {
2023-08-01 10:43:39 -04:00
rootCmd . AddCommand ( ManCmd )
2023-07-12 12:07:31 -04:00
2026-04-09 21:48:21 +08:00
cfg , _ := loadConfig ( )
2026-04-10 00:39:06 +08:00
_ = cfg
2026-04-09 21:48:21 +08:00
2023-07-31 10:32:02 -04:00
rootCmd . Flags ( ) . StringSliceVar ( & bcc , "bcc" , [ ] string { } , "BCC recipients" )
rootCmd . Flags ( ) . StringSliceVar ( & cc , "cc" , [ ] string { } , "CC recipients" )
2023-06-15 10:30:20 -04:00
rootCmd . Flags ( ) . StringSliceVarP ( & attachments , "attach" , "a" , [ ] string { } , "Email's attachments" )
2023-06-15 11:27:20 -04:00
rootCmd . Flags ( ) . StringSliceVarP ( & to , "to" , "t" , [ ] string { } , "Recipients" )
rootCmd . Flags ( ) . StringVarP ( & body , "body" , "b" , "" , "Email's contents" )
2023-07-31 10:32:02 -04:00
envFrom := os . Getenv ( PopFrom )
2026-04-09 21:48:21 +08:00
if envFrom == "" {
2026-04-10 00:39:06 +08:00
envFrom = getDefaultFromEmail ( )
2026-04-09 21:48:21 +08:00
}
from = envFrom
2023-07-31 10:32:02 -04:00
rootCmd . Flags ( ) . StringVarP ( & from , "from" , "f" , envFrom , "Email's sender" + commentStyle . Render ( "($" + PopFrom + ")" ) )
2023-06-15 10:30:20 -04:00
rootCmd . Flags ( ) . StringVarP ( & subject , "subject" , "s" , "" , "Email's subject" )
2023-07-31 10:32:02 -04:00
rootCmd . Flags ( ) . BoolVar ( & preview , "preview" , false , "Whether to preview the email before sending" )
envUnsafe := os . Getenv ( PopUnsafeHTML ) == "true"
2026-04-09 21:48:21 +08:00
if ! envUnsafe {
envUnsafe = cfg . UnsafeHTML
}
2023-07-20 23:42:37 +03:00
rootCmd . Flags ( ) . BoolVarP ( & unsafe , "unsafe" , "u" , envUnsafe , "Whether to allow unsafe HTML in the email body, also enable some extra markdown features (Experimental)" )
2023-07-31 10:32:02 -04:00
envSignature := os . Getenv ( PopSignature )
2026-04-09 21:48:21 +08:00
if envSignature == "" {
envSignature = cfg . Signature
}
2023-07-31 10:32:02 -04:00
rootCmd . Flags ( ) . StringVarP ( & signature , "signature" , "x" , envSignature , "Signature to display at the end of the email." + commentStyle . Render ( "($" + PopSignature + ")" ) )
envSMTPHost := os . Getenv ( PopSMTPHost )
envSMTPPort , _ := strconv . Atoi ( os . Getenv ( PopSMTPPort ) )
2026-04-10 00:39:06 +08:00
envSMTPUsername := os . Getenv ( PopSMTPUsername )
envSMTPPassword := os . Getenv ( PopSMTPPassword )
envSMTPEncryption := os . Getenv ( PopSMTPEncryption )
envInsecureSkipVerify := os . Getenv ( PopSMTPInsecureSkipVerify ) == "true"
defaultAccounts , _ := getAccounts ( )
defaultAccount := getDefaultAccount ( defaultAccounts , cfg . From . Account )
if envSMTPHost == "" && defaultAccount != nil {
envSMTPHost = defaultAccount . SMTP . Host
}
if envSMTPPort == 0 && defaultAccount != nil {
envSMTPPort = defaultAccount . SMTP . Port
2026-04-09 21:48:21 +08:00
if envSMTPPort == 0 {
envSMTPPort = 587
}
2023-07-31 10:32:02 -04:00
}
2026-04-10 00:39:06 +08:00
if envSMTPUsername == "" && defaultAccount != nil {
envSMTPUsername = defaultAccount . SMTP . Username
2026-04-09 21:48:21 +08:00
}
2026-04-10 00:39:06 +08:00
if envSMTPPassword == "" && defaultAccount != nil {
envSMTPPassword = defaultAccount . SMTP . Password
2026-04-09 21:48:21 +08:00
}
2026-04-10 00:39:06 +08:00
if envSMTPEncryption == "" && defaultAccount != nil {
envSMTPEncryption = defaultAccount . SMTP . Encryption
2026-04-09 21:48:21 +08:00
if envSMTPEncryption == "" {
envSMTPEncryption = "starttls"
}
}
2026-04-10 00:39:06 +08:00
if ! envInsecureSkipVerify && defaultAccount != nil {
envInsecureSkipVerify = defaultAccount . SMTP . InsecureSkipVerify
2026-04-09 21:48:21 +08:00
}
2026-04-10 00:39:06 +08:00
smtpHost = envSMTPHost
smtpPort = envSMTPPort
smtpUsername = envSMTPUsername
smtpPassword = envSMTPPassword
smtpEncryption = envSMTPEncryption
2026-04-09 21:48:21 +08:00
smtpInsecureSkipVerify = envInsecureSkipVerify
2026-04-10 00:39:06 +08:00
rootCmd . Flags ( ) . StringVarP ( & smtpHost , "smtp.host" , "H" , envSMTPHost , "Host of the SMTP server" + commentStyle . Render ( "($" + PopSMTPHost + ")" ) )
rootCmd . Flags ( ) . IntVarP ( & smtpPort , "smtp.port" , "P" , envSMTPPort , "Port of the SMTP server" + commentStyle . Render ( "($" + PopSMTPPort + ")" ) )
rootCmd . Flags ( ) . StringVarP ( & smtpUsername , "smtp.username" , "U" , envSMTPUsername , "Username of the SMTP server" + commentStyle . Render ( "($" + PopSMTPUsername + ")" ) )
rootCmd . Flags ( ) . StringVarP ( & smtpPassword , "smtp.password" , "p" , envSMTPPassword , "Password of the SMTP server" + commentStyle . Render ( "($" + PopSMTPPassword + ")" ) )
rootCmd . Flags ( ) . StringVarP ( & smtpEncryption , "smtp.encryption" , "e" , envSMTPEncryption , "Encryption type of the SMTP server (starttls, ssl, or none)" + commentStyle . Render ( "($" + PopSMTPEncryption + ")" ) )
2023-07-31 10:32:02 -04:00
rootCmd . Flags ( ) . BoolVarP ( & smtpInsecureSkipVerify , "smtp.insecure" , "i" , envInsecureSkipVerify , "Skip TLS verification with SMTP server" + commentStyle . Render ( "($" + PopSMTPInsecureSkipVerify + ")" ) )
envResendAPIKey := os . Getenv ( ResendAPIKey )
rootCmd . Flags ( ) . StringVarP ( & resendAPIKey , "resend.key" , "r" , envResendAPIKey , "API key for the Resend.com" + commentStyle . Render ( "($" + ResendAPIKey + ")" ) )
2023-07-12 12:07:31 -04:00
rootCmd . CompletionOptions . HiddenDefaultCmd = true
2026-04-09 21:48:21 +08:00
var configCmd = & cobra . Command {
Use : "config" ,
Short : "配置或重新配置 Pop" ,
Long : ` 打开交互式配置向导来设置或更新 Pop 的配置。 ` ,
Args : cobra . NoArgs ,
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
return runOnboarding ( )
} ,
}
rootCmd . AddCommand ( configCmd )
var inboxCmd = & cobra . Command {
Use : "inbox" ,
Short : "打开收件箱" ,
Long : ` 查看配置邮箱的收件箱,显示未读邮件列表。 ` ,
Args : cobra . NoArgs ,
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
return runInbox ( 7 )
} ,
}
inboxCmd . Flags ( ) . IntP ( "days" , "d" , 7 , "显示最近几天的未读邮件" )
rootCmd . AddCommand ( inboxCmd )
var historyCmd = & cobra . Command {
Use : "history" ,
Short : "查看发送历史" ,
Long : ` 查看已发送或草稿邮件的历史记录。 ` ,
Args : cobra . NoArgs ,
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
return runHistory ( )
} ,
}
rootCmd . AddCommand ( historyCmd )
2023-07-12 12:07:31 -04:00
if len ( CommitSHA ) >= 7 { //nolint:gomnd
vt := rootCmd . VersionTemplate ( )
rootCmd . SetVersionTemplate ( vt [ : len ( vt ) - 1 ] + " (" + CommitSHA [ 0 : 7 ] + ")\n" )
}
if Version == "" {
if info , ok := debug . ReadBuildInfo ( ) ; ok && info . Main . Sum != "" {
Version = info . Main . Version
} else {
Version = "unknown (built from source)"
}
}
rootCmd . Version = Version
2023-06-15 10:30:20 -04:00
}
2023-06-13 23:31:19 -04:00
func main ( ) {
err := rootCmd . Execute ( )
if err != nil {
os . Exit ( 1 )
}
}