gogcli/internal/cmd/gmail_sendas.go
salmonumbrella 3371e3f3ad
feat(cli): agent ergonomics + gmail watch exclude labels (#201)
* feat(cli): improve agent ergonomics

* fix(cli): address code review findings

- Fix nil pointer dereference in confirmDestructive when flags is nil
- Deduplicate dry-run logic by delegating to dryRunExit
- Remove deprecated net.Error.Temporary() call (dead since Go 1.18)
- Add unit tests for resolveTasklistID and resolveCalendarID

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve PR #201 conflicts and follow-ups (#201) (thanks @salmonumbrella)

* fix: resolve rebase fallout for PR #201 landing (#201) (thanks @salmonumbrella)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-14 03:09:49 +01:00

342 lines
8.7 KiB
Go

package cmd
import (
"context"
"errors"
"fmt"
"os"
"strings"
"text/tabwriter"
"github.com/alecthomas/kong"
"google.golang.org/api/gmail/v1"
"github.com/steipete/gogcli/internal/outfmt"
"github.com/steipete/gogcli/internal/ui"
)
type GmailSendAsCmd struct {
List GmailSendAsListCmd `cmd:"" name:"list" aliases:"ls" help:"List send-as aliases"`
Get GmailSendAsGetCmd `cmd:"" name:"get" aliases:"info,show" help:"Get details of a send-as alias"`
Create GmailSendAsCreateCmd `cmd:"" name:"create" aliases:"add,new" help:"Create a new send-as alias"`
Verify GmailSendAsVerifyCmd `cmd:"" name:"verify" aliases:"resend" help:"Resend verification email for a send-as alias"`
Delete GmailSendAsDeleteCmd `cmd:"" name:"delete" aliases:"rm,del,remove" help:"Delete a send-as alias"`
Update GmailSendAsUpdateCmd `cmd:"" name:"update" aliases:"edit,set" help:"Update a send-as alias"`
}
type GmailSendAsListCmd struct{}
const sendAsYes = "yes"
func (c *GmailSendAsListCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newGmailService(ctx, account)
if err != nil {
return err
}
resp, err := svc.Users.Settings.SendAs.List("me").Do()
if err != nil {
return err
}
if outfmt.IsJSON(ctx) {
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{"sendAs": resp.SendAs})
}
if len(resp.SendAs) == 0 {
u.Err().Println("No send-as aliases")
return nil
}
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
fmt.Fprintln(tw, "EMAIL\tDISPLAY NAME\tDEFAULT\tVERIFIED\tTREAT AS ALIAS")
for _, sa := range resp.SendAs {
isDefault := ""
if sa.IsDefault {
isDefault = sendAsYes
}
verified := "pending"
if sa.VerificationStatus == gmailVerificationAccepted {
verified = sendAsYes
}
treatAsAlias := ""
if sa.TreatAsAlias {
treatAsAlias = sendAsYes
}
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n",
sa.SendAsEmail, sa.DisplayName, isDefault, verified, treatAsAlias)
}
_ = tw.Flush()
return nil
}
type GmailSendAsGetCmd struct {
Email string `arg:"" name:"email" help:"Send-as email"`
}
func (c *GmailSendAsGetCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
account, err := requireAccount(flags)
if err != nil {
return err
}
sendAsEmail := strings.TrimSpace(c.Email)
if sendAsEmail == "" {
return errors.New("email is required")
}
svc, err := newGmailService(ctx, account)
if err != nil {
return err
}
sa, err := svc.Users.Settings.SendAs.Get("me", sendAsEmail).Do()
if err != nil {
return err
}
if outfmt.IsJSON(ctx) {
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{"sendAs": sa})
}
u.Out().Printf("send_as_email\t%s", sa.SendAsEmail)
u.Out().Printf("display_name\t%s", sa.DisplayName)
u.Out().Printf("reply_to\t%s", sa.ReplyToAddress)
u.Out().Printf("signature\t%s", sa.Signature)
u.Out().Printf("is_primary\t%t", sa.IsPrimary)
u.Out().Printf("is_default\t%t", sa.IsDefault)
u.Out().Printf("treat_as_alias\t%t", sa.TreatAsAlias)
u.Out().Printf("verification_status\t%s", sa.VerificationStatus)
return nil
}
type GmailSendAsCreateCmd struct {
Email string `arg:"" name:"email" help:"Send-as email"`
DisplayName string `name:"display-name" help:"Name that appears in the From field"`
ReplyTo string `name:"reply-to" help:"Reply-to address (optional)"`
Signature string `name:"signature" help:"HTML signature for emails sent from this alias"`
TreatAsAlias bool `name:"treat-as-alias" help:"Treat as alias (replies sent from Gmail web)" default:"true"`
}
func (c *GmailSendAsCreateCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
sendAsEmail := strings.TrimSpace(c.Email)
if sendAsEmail == "" {
return errors.New("email is required")
}
sendAs := &gmail.SendAs{
SendAsEmail: sendAsEmail,
DisplayName: c.DisplayName,
ReplyToAddress: c.ReplyTo,
Signature: c.Signature,
TreatAsAlias: c.TreatAsAlias,
}
if err := dryRunExit(ctx, flags, "gmail.sendas.create", map[string]any{
"send_as": sendAs,
}); err != nil {
return err
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newGmailService(ctx, account)
if err != nil {
return err
}
created, err := svc.Users.Settings.SendAs.Create("me", sendAs).Do()
if err != nil {
return err
}
if outfmt.IsJSON(ctx) {
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{"sendAs": created})
}
u.Out().Printf("send_as_email\t%s", created.SendAsEmail)
u.Out().Printf("verification_status\t%s", created.VerificationStatus)
u.Err().Println("Verification email sent. Check your inbox to complete setup.")
return nil
}
type GmailSendAsVerifyCmd struct {
Email string `arg:"" name:"email" help:"Send-as email"`
}
func (c *GmailSendAsVerifyCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
sendAsEmail := strings.TrimSpace(c.Email)
if sendAsEmail == "" {
return errors.New("email is required")
}
if err := dryRunExit(ctx, flags, "gmail.sendas.verify", map[string]any{
"email": sendAsEmail,
}); err != nil {
return err
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newGmailService(ctx, account)
if err != nil {
return err
}
err = svc.Users.Settings.SendAs.Verify("me", sendAsEmail).Do()
if err != nil {
return err
}
if outfmt.IsJSON(ctx) {
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
"email": sendAsEmail,
"message": "Verification email sent",
})
}
u.Out().Printf("Verification email sent to %s", sendAsEmail)
return nil
}
type GmailSendAsDeleteCmd struct {
Email string `arg:"" name:"email" help:"Send-as email"`
}
func (c *GmailSendAsDeleteCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
sendAsEmail := strings.TrimSpace(c.Email)
if sendAsEmail == "" {
return errors.New("email is required")
}
if confirmErr := confirmDestructive(ctx, flags, fmt.Sprintf("delete gmail send-as alias %s", sendAsEmail)); confirmErr != nil {
return confirmErr
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newGmailService(ctx, account)
if err != nil {
return err
}
err = svc.Users.Settings.SendAs.Delete("me", sendAsEmail).Do()
if err != nil {
return err
}
if outfmt.IsJSON(ctx) {
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
"email": sendAsEmail,
"deleted": true,
})
}
u.Out().Printf("Deleted send-as alias: %s", sendAsEmail)
return nil
}
type GmailSendAsUpdateCmd struct {
Email string `arg:"" name:"email" help:"Send-as email"`
DisplayName string `name:"display-name" help:"Name that appears in the From field"`
ReplyTo string `name:"reply-to" help:"Reply-to address"`
Signature string `name:"signature" help:"HTML signature"`
TreatAsAlias bool `name:"treat-as-alias" help:"Treat as alias" default:"true"`
MakeDefault bool `name:"make-default" help:"Make this the default send-as address"`
}
func (c *GmailSendAsUpdateCmd) Run(ctx context.Context, kctx *kong.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
sendAsEmail := strings.TrimSpace(c.Email)
if sendAsEmail == "" {
return errors.New("email is required")
}
updates := map[string]any{}
if flagProvided(kctx, "display-name") {
updates["display_name"] = c.DisplayName
}
if flagProvided(kctx, "reply-to") {
updates["reply_to"] = c.ReplyTo
}
if flagProvided(kctx, "signature") {
updates["signature"] = c.Signature
}
if flagProvided(kctx, "treat-as-alias") {
updates["treat_as_alias"] = c.TreatAsAlias
}
if flagProvided(kctx, "make-default") {
updates["make_default"] = c.MakeDefault
}
if err := dryRunExit(ctx, flags, "gmail.sendas.update", map[string]any{
"email": sendAsEmail,
"updates": updates,
}); err != nil {
return err
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newGmailService(ctx, account)
if err != nil {
return err
}
// Get current settings first
current, err := svc.Users.Settings.SendAs.Get("me", sendAsEmail).Do()
if err != nil {
return err
}
// Update only provided fields
if flagProvided(kctx, "display-name") {
current.DisplayName = c.DisplayName
}
if flagProvided(kctx, "reply-to") {
current.ReplyToAddress = c.ReplyTo
}
if flagProvided(kctx, "signature") {
current.Signature = c.Signature
}
if flagProvided(kctx, "treat-as-alias") {
current.TreatAsAlias = c.TreatAsAlias
}
if flagProvided(kctx, "make-default") {
current.IsDefault = c.MakeDefault
}
updated, err := svc.Users.Settings.SendAs.Update("me", sendAsEmail, current).Do()
if err != nil {
return err
}
if outfmt.IsJSON(ctx) {
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{"sendAs": updated})
}
u.Out().Printf("Updated send-as alias: %s", updated.SendAsEmail)
return nil
}