Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 140 additions & 11 deletions extensions/ty-email/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,30 +66,38 @@ func runInit(cmd *cobra.Command, args []string) error {
}

err := huh.NewSelect[string]().
Title("How will you receive emails?").
Title("How will you receive messages?").
Options(
huh.NewOption("Gmail", "gmail"),
huh.NewOption("Other IMAP (Fastmail, ProtonMail, etc.)", "imap"),
huh.NewOption("SMS via Twilio", "twilio"),
).
Value(&emailProvider).
Run()
if err != nil {
return err
}

// Both use IMAP adapter, Gmail just has pre-filled servers
cfg.Adapter.Type = "imap"

if emailProvider == "gmail" {
if err := configureGmailIMAP(cfg, home); err != nil {
if emailProvider == "twilio" {
cfg.Adapter.Type = "twilio"
if err := configureTwilio(cfg); err != nil {
return err
}
} else {
if err := configureIMAP(cfg, home); err != nil {
return err
}
if err := configureSMTP(cfg); err != nil {
return err
// Both use IMAP adapter, Gmail just has pre-filled servers
cfg.Adapter.Type = "imap"

if emailProvider == "gmail" {
if err := configureGmailIMAP(cfg, home); err != nil {
return err
}
} else {
if err := configureIMAP(cfg, home); err != nil {
return err
}
if err := configureSMTP(cfg); err != nil {
return err
}
}
}

Expand Down Expand Up @@ -356,6 +364,127 @@ Note: ty-email replies will appear in your Inbox (not the ty-email label).
return nil
}

func configureTwilio(cfg *Config) error {
fmt.Println(titleStyle.Render("\n📱 Twilio SMS"))

if cfg.Adapter.Twilio == nil {
cfg.Adapter.Twilio = &adapter.TwilioConfig{}
}

accountSIDCmd := cfg.Adapter.Twilio.AccountSIDCmd
authTokenCmd := cfg.Adapter.Twilio.AuthTokenCmd
phoneNumber := cfg.Adapter.Twilio.PhoneNumber
webhookListen := cfg.Adapter.Twilio.WebhookListen
if webhookListen == "" {
webhookListen = ":8080"
}

form := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("Account SID Command").
Description("Command to retrieve your Twilio Account SID").
Placeholder("op read 'op://Private/Twilio/account_sid'").
Value(&accountSIDCmd).
Validate(func(s string) error {
if s == "" {
return fmt.Errorf("account SID command is required")
}
return nil
}),

huh.NewInput().
Title("Auth Token Command").
Description("Command to retrieve your Twilio Auth Token").
Placeholder("op read 'op://Private/Twilio/auth_token'").
Value(&authTokenCmd).
Validate(func(s string) error {
if s == "" {
return fmt.Errorf("auth token command is required")
}
return nil
}),

huh.NewInput().
Title("Twilio Phone Number").
Description("Your Twilio phone number (e.g., +15551234567)").
Value(&phoneNumber).
Validate(func(s string) error {
if s == "" || !strings.HasPrefix(s, "+") {
return fmt.Errorf("enter a phone number starting with + (e.g., +15551234567)")
}
return nil
}),

huh.NewInput().
Title("Webhook Listen Address").
Description("Local address for inbound SMS webhook").
Value(&webhookListen),
),
)

if err := form.Run(); err != nil {
return err
}

// Test Account SID command
fmt.Print("Testing Account SID command... ")
out, err := exec.Command("sh", "-c", accountSIDCmd).Output()
if err != nil {
fmt.Println(errorStyle.Render("FAILED: " + err.Error()))
return fmt.Errorf("account SID command failed - please check and re-run init")
} else if len(strings.TrimSpace(string(out))) == 0 {
fmt.Println(errorStyle.Render("FAILED: command returned empty"))
return fmt.Errorf("account SID command returned empty - please check and re-run init")
}
fmt.Println(successStyle.Render("OK"))

// Test Auth Token command
fmt.Print("Testing Auth Token command... ")
out, err = exec.Command("sh", "-c", authTokenCmd).Output()
if err != nil {
fmt.Println(errorStyle.Render("FAILED: " + err.Error()))
return fmt.Errorf("auth token command failed - please check and re-run init")
} else if len(strings.TrimSpace(string(out))) == 0 {
fmt.Println(errorStyle.Render("FAILED: command returned empty"))
return fmt.Errorf("auth token command returned empty - please check and re-run init")
}
fmt.Println(successStyle.Render("OK"))

cfg.Adapter.Twilio.AccountSIDCmd = accountSIDCmd
cfg.Adapter.Twilio.AuthTokenCmd = authTokenCmd
cfg.Adapter.Twilio.PhoneNumber = phoneNumber
cfg.Adapter.Twilio.WebhookListen = webhookListen
cfg.Adapter.Twilio.WebhookPath = "/sms"

// Security - add a prompt for allowed senders
fmt.Println(infoStyle.Render("\nFor security, only SMS from specific phone numbers will be processed."))

var allowedSender string
err = huh.NewInput().
Title("Your Phone Number").
Description("SMS from this number will be processed (e.g., +15559876543)").
Value(&allowedSender).
Validate(func(s string) error {
if s == "" || !strings.HasPrefix(s, "+") {
return fmt.Errorf("enter a phone number starting with +")
}
return nil
}).
Run()
if err != nil {
return err
}

cfg.Security.AllowedSenders = []string{allowedSender}

fmt.Println(successStyle.Render("\n✓ Twilio configured (only SMS from " + allowedSender + " will be processed)"))
fmt.Println(infoStyle.Render("\nNote: Configure your Twilio number's webhook URL to point to this server."))
fmt.Println(infoStyle.Render(" Webhook URL: http://your-server" + webhookListen + "/sms"))

return nil
}

func configureSMTP(cfg *Config) error {
fmt.Println(titleStyle.Render("\n📤 Outbound Email (SMTP)"))

Expand Down
5 changes: 5 additions & 0 deletions extensions/ty-email/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,11 @@ func setupAdapter(cfg *Config, logger *slog.Logger) (adapter.Adapter, error) {
return nil, fmt.Errorf("Gmail config required")
}
return adapter.NewGmailAdapter(cfg.Adapter.Gmail, logger), nil
case "twilio":
if cfg.Adapter.Twilio == nil {
return nil, fmt.Errorf("Twilio config required")
}
return adapter.NewTwilioAdapter(cfg.Adapter.Twilio, logger), nil
default:
return nil, fmt.Errorf("unsupported adapter type: %s", cfg.Adapter.Type)
}
Expand Down
14 changes: 12 additions & 2 deletions extensions/ty-email/config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

# Email input adapter
adapter:
type: imap # Options: imap, gmail, webhook
type: imap # Options: imap, gmail, webhook, twilio

# IMAP configuration (for Fastmail, ProtonMail, etc.)
imap:
Expand All @@ -28,7 +28,17 @@ adapter:
# path: /webhook/email
# secret: your-webhook-secret

# SMTP for sending replies
# Twilio SMS configuration
# twilio:
# account_sid_cmd: "op read 'op://Private/Twilio/account_sid'"
# auth_token_cmd: "op read 'op://Private/Twilio/auth_token'"
# phone_number: "+15551234567" # Your Twilio phone number
# webhook_listen: ":8080" # Local server for inbound SMS
# webhook_path: "/sms" # Webhook endpoint path
# validate_hmac: true # Validate Twilio request signatures
# webhook_url: "https://example.com/sms" # Public URL (for signature validation)

# SMTP for sending replies (not needed for Twilio adapter)
smtp:
server: smtp.fastmail.com:587
username: you@yourdomain.com
Expand Down
3 changes: 2 additions & 1 deletion extensions/ty-email/internal/adapter/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,12 @@ type Adapter interface {

// Config holds adapter configuration.
type Config struct {
Type string `yaml:"type"` // "gmail", "imap", "webhook"
Type string `yaml:"type"` // "gmail", "imap", "webhook", "twilio"

Gmail *GmailConfig `yaml:"gmail,omitempty"`
IMAP *IMAPConfig `yaml:"imap,omitempty"`
Webhook *WebhookConfig `yaml:"webhook,omitempty"`
Twilio *TwilioConfig `yaml:"twilio,omitempty"`
}

// GmailConfig holds Gmail OAuth configuration.
Expand Down
Loading