diff --git a/cmd/oo/mails.go b/cmd/oo/mails.go index 3b90c5a..d6d5b4d 100644 --- a/cmd/oo/mails.go +++ b/cmd/oo/mails.go @@ -22,6 +22,7 @@ func init() { mailsCmd.AddCommand(mailsFoldersCmd()) mailsCmd.AddCommand(mailsListCmd()) mailsCmd.AddCommand(mailsGetCmd()) + mailsCmd.AddCommand(mailsDownloadAttachmentCmd()) mailsCmd.AddCommand(mailsDraftCmd()) mailsCmd.AddCommand(mailsAttachCmd()) mailsCmd.AddCommand(mailsDraftInvoiceCmd()) @@ -131,6 +132,48 @@ func mailsGetCmd() *cobra.Command { } } +func mailsDownloadAttachmentCmd() *cobra.Command { + var outPath string + cmd := &cobra.Command{ + Use: "download-attachment ATTACHMENT_ID", + Short: "Download a mail attachment by attachment id", + Long: `Download a raw attachment from OnlyOffice Mail's download.ashx handler. + +Example: + oo mails download-attachment 12345 --out /tmp/attach.bin +`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if strings.TrimSpace(outPath) == "" { + return fmt.Errorf("--out is required") + } + c, err := newOO(cmd) + if err != nil { + return err + } + body, err := c.DownloadMailAttachment(cmd.Context(), args[0]) + if err != nil { + return err + } + if err := writeMailAttachment(outPath, body); err != nil { + return err + } + if outputFormat == "json" { + printObject(map[string]any{ + "attachmentId": args[0], + "bytes": len(body), + "path": outPath, + }) + return nil + } + fmt.Printf("saved %d bytes to %s\n", len(body), outPath) + return nil + }, + } + cmd.Flags().StringVar(&outPath, "out", "", "output file path") + return cmd +} + func mailsDraftCmd() *cobra.Command { var from, to, cc, bcc, subject, body, html string var id int64 @@ -297,6 +340,13 @@ func formatInvoiceCostEUR(v any) string { return s } +func writeMailAttachment(path string, body []byte) error { + if strings.TrimSpace(path) == "" { + return fmt.Errorf("attachment output path is required") + } + return os.WriteFile(path, body, 0o644) +} + func mailsDeleteCmd() *cobra.Command { return &cobra.Command{ Use: "delete ID [ID...]", diff --git a/cmd/oo/mails_cli_test.go b/cmd/oo/mails_cli_test.go new file mode 100644 index 0000000..dd69516 --- /dev/null +++ b/cmd/oo/mails_cli_test.go @@ -0,0 +1,28 @@ +package main + +import ( + "os" + "path/filepath" + "testing" +) + +func TestWriteMailAttachment(t *testing.T) { + path := filepath.Join(t.TempDir(), "attach.bin") + body := []byte("payload") + if err := writeMailAttachment(path, body); err != nil { + t.Fatalf("writeMailAttachment: %v", err) + } + got, err := os.ReadFile(path) + if err != nil { + t.Fatalf("ReadFile: %v", err) + } + if string(got) != string(body) { + t.Fatalf("body = %q", got) + } +} + +func TestWriteMailAttachmentRequiresPath(t *testing.T) { + if err := writeMailAttachment("", []byte("x")); err == nil { + t.Fatal("expected error for empty path") + } +} diff --git a/cmd/oo/main.go b/cmd/oo/main.go index 13c38fc..30ba5b6 100644 --- a/cmd/oo/main.go +++ b/cmd/oo/main.go @@ -13,7 +13,7 @@ // oo cases list | create | delete | member-add // oo crm-tasks list | create | delete | categories // oo crm cleanup -// oo mails accounts | folders | list | get | draft | attach | draft-invoice | delete +// oo mails accounts | folders | list | get | download-attachment | draft | attach | draft-invoice | delete // oo invoices list | get | create | update | pdf | pdf-cleanup | status | delete | items … // // CRM association rules: docs/crm-associations.md