feat(mail): draft, attach, and draft-invoice CLI
Save OO Mail drafts via body field, attach Files documents, and one-shot invoice PDF regenerate + draft for client billing emails.
This commit is contained in:
@@ -5,6 +5,7 @@ package onlyoffice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
@@ -96,11 +97,156 @@ func (c *Client) GetMailMessage(ctx context.Context, messageID string) (map[stri
|
||||
}
|
||||
|
||||
// RemoveMailMessages deletes messages by id (PUT /api/2.0/mail/messages/remove).
|
||||
// The API response "response" field may be a number or object; success is HTTP 2xx.
|
||||
func (c *Client) RemoveMailMessages(ctx context.Context, ids ...int) (map[string]any, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, fmt.Errorf("RemoveMailMessages: at least one id is required")
|
||||
}
|
||||
return c.putJSONObject(ctx, "/api/2.0/mail/messages/remove", map[string]any{"ids": ids})
|
||||
raw, err := c.putJSON(ctx, "/api/2.0/mail/messages/remove", map[string]any{"ids": ids})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := map[string]any{"ok": true, "ids": ids}
|
||||
if resp, err := responseField(raw, "response"); err == nil {
|
||||
var n json.Number
|
||||
if json.Unmarshal(resp, &n) == nil {
|
||||
out["response"] = n.String()
|
||||
} else {
|
||||
var m map[string]any
|
||||
if json.Unmarshal(resp, &m) == nil {
|
||||
out["response"] = m
|
||||
} else {
|
||||
out["response"] = string(resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// SaveMailDraftParams creates or updates a draft via PUT /api/2.0/mail/drafts/save.
|
||||
// Id 0 creates a new draft. Body is HTML; the API field name is "body" (not htmlBody).
|
||||
type SaveMailDraftParams struct {
|
||||
ID int64 // 0 = create
|
||||
From string // mailbox address, e.g. eslider@gmail.com
|
||||
To string // comma-separated or single address
|
||||
Cc string
|
||||
Bcc string
|
||||
Subject string
|
||||
Body string // HTML
|
||||
}
|
||||
|
||||
// SaveMailDraft saves a draft message. Returns the saved message map (incl. id).
|
||||
func (c *Client) SaveMailDraft(ctx context.Context, p SaveMailDraftParams) (map[string]any, error) {
|
||||
if strings.TrimSpace(p.To) == "" {
|
||||
return nil, fmt.Errorf("SaveMailDraft: to is required")
|
||||
}
|
||||
if strings.TrimSpace(p.From) == "" {
|
||||
from, err := c.defaultMailFrom(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.From = from
|
||||
}
|
||||
body := map[string]any{
|
||||
"id": p.ID,
|
||||
"from": p.From,
|
||||
"to": p.To,
|
||||
"cc": p.Cc,
|
||||
"bcc": p.Bcc,
|
||||
"subject": p.Subject,
|
||||
"body": p.Body,
|
||||
}
|
||||
return c.putJSONObject(ctx, "/api/2.0/mail/drafts/save", body)
|
||||
}
|
||||
|
||||
func (c *Client) defaultMailFrom(ctx context.Context) (string, error) {
|
||||
accounts, err := c.ListMailAccounts(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var fallback string
|
||||
for _, a := range accounts {
|
||||
email := strings.TrimSpace(stringFromMap(a, "email"))
|
||||
if email == "" || email == "<nil>" {
|
||||
continue
|
||||
}
|
||||
if boolField(a, "enabled") || boolField(a, "isDefault") {
|
||||
return email, nil
|
||||
}
|
||||
if fallback == "" {
|
||||
fallback = email
|
||||
}
|
||||
}
|
||||
if fallback == "" {
|
||||
return "", fmt.Errorf("SaveMailDraft: from is required (no mailbox on account)")
|
||||
}
|
||||
return fallback, nil
|
||||
}
|
||||
|
||||
// AttachMailDocument attaches an OnlyOffice Files document to a mail message/draft.
|
||||
// fileID is the Documents/CRM file id (e.g. from InvoicePDFFile).
|
||||
func (c *Client) AttachMailDocument(ctx context.Context, messageID string, fileID int64) (map[string]any, error) {
|
||||
id := strings.TrimSpace(messageID)
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("AttachMailDocument: message id is required")
|
||||
}
|
||||
if fileID <= 0 {
|
||||
return nil, fmt.Errorf("AttachMailDocument: file id is required")
|
||||
}
|
||||
fields := url.Values{}
|
||||
fields.Set("fileId", strconv.FormatInt(fileID, 10))
|
||||
return c.postFormObject(ctx, fmt.Sprintf("/api/2.0/mail/messages/%s/document", url.PathEscape(id)), fields)
|
||||
}
|
||||
|
||||
// InvoicePDFFile regenerates/returns the invoice PDF file metadata (id, title, viewUrl).
|
||||
func (c *Client) InvoicePDFFile(ctx context.Context, invoiceID string) (map[string]any, error) {
|
||||
id := strings.TrimSpace(invoiceID)
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("InvoicePDFFile: invoice id is required")
|
||||
}
|
||||
return c.ResponseObject(ctx, fmt.Sprintf("/api/2.0/crm/invoice/%s/pdf", url.PathEscape(id)))
|
||||
}
|
||||
|
||||
// PlainTextToMailHTML turns plain text into simple HTML paragraphs for drafts.
|
||||
// If s already looks like HTML, it is returned unchanged.
|
||||
func PlainTextToMailHTML(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.Contains(s, "<") && strings.Contains(s, ">") {
|
||||
return s
|
||||
}
|
||||
parts := strings.Split(s, "\n\n")
|
||||
var b strings.Builder
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
b.WriteString("<p>")
|
||||
b.WriteString(strings.ReplaceAll(p, "\n", "<br/>"))
|
||||
b.WriteString("</p>\n")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Int64FromMap coerces OnlyOffice numeric id fields (float64/int/string) to int64.
|
||||
func Int64FromMap(m map[string]any, key string) int64 {
|
||||
switch v := m[key].(type) {
|
||||
case float64:
|
||||
return int64(v)
|
||||
case int64:
|
||||
return v
|
||||
case int:
|
||||
return int64(v)
|
||||
case string:
|
||||
n, _ := strconv.ParseInt(strings.TrimSpace(v), 10, 64)
|
||||
return n
|
||||
default:
|
||||
n, _ := strconv.ParseInt(fmt.Sprint(m[key]), 10, 64)
|
||||
return n
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveMailFolder maps a CLI folder name or numeric string to a folder id.
|
||||
|
||||
Reference in New Issue
Block a user