reconcile(github): sync GitHub-only commits into Gitea main (#144) #6
@@ -26,6 +26,7 @@ func init() {
|
|||||||
mailsCmd.AddCommand(mailsDraftCmd())
|
mailsCmd.AddCommand(mailsDraftCmd())
|
||||||
mailsCmd.AddCommand(mailsAttachCmd())
|
mailsCmd.AddCommand(mailsAttachCmd())
|
||||||
mailsCmd.AddCommand(mailsDraftInvoiceCmd())
|
mailsCmd.AddCommand(mailsDraftInvoiceCmd())
|
||||||
|
mailsCmd.AddCommand(mailsSendCmd())
|
||||||
mailsCmd.AddCommand(mailsDeleteCmd())
|
mailsCmd.AddCommand(mailsDeleteCmd())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,6 +348,71 @@ func writeMailAttachment(path string, body []byte) error {
|
|||||||
return os.WriteFile(path, body, 0o644)
|
return os.WriteFile(path, body, 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mailsSendCmd() *cobra.Command {
|
||||||
|
var from, to, cc, bcc, subject, body, html string
|
||||||
|
var id int64
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "send",
|
||||||
|
Short: "Send a mail message (OnlyOffice Mail)",
|
||||||
|
Long: `Send via PUT /api/2.0/mail/messages/send.json.
|
||||||
|
|
||||||
|
oo mails send --id 7803 --body "…" # send referencing a draft id
|
||||||
|
oo mails send --to a@b.com --subject "…" --body "…"
|
||||||
|
oo mails send --id 7803 --to a@b.com --subject "…" --body "…" --cc x@y.com
|
||||||
|
|
||||||
|
IMPORTANT: send.json does NOT copy subject/body from the referenced draft — the
|
||||||
|
content must be in this request (--subject/--body). Cc/Bcc are omitted when empty
|
||||||
|
(the API 400s on empty strings). The API send does not append the UI signature —
|
||||||
|
put the chat line in --body if needed.
|
||||||
|
`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if to == "" && id == 0 {
|
||||||
|
return fmt.Errorf("--to is required (or --id of an existing draft)")
|
||||||
|
}
|
||||||
|
htmlBody := body
|
||||||
|
if html != "" {
|
||||||
|
htmlBody = html
|
||||||
|
}
|
||||||
|
if htmlBody == "" && id != 0 {
|
||||||
|
// The send.json endpoint does NOT copy subject/body from the
|
||||||
|
// referenced draft — an empty body here sends an empty message.
|
||||||
|
// Warn instead of silently mailing an empty email.
|
||||||
|
return fmt.Errorf("--body/--html is required when sending by --id (send.json needs the content in the request)")
|
||||||
|
}
|
||||||
|
if htmlBody == "" && to == "" {
|
||||||
|
return fmt.Errorf("--body is required for a fresh message")
|
||||||
|
}
|
||||||
|
c, err := newOO(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
raw, err := c.SendMail(cmd.Context(), onlyoffice.SendMailParams{
|
||||||
|
ID: id,
|
||||||
|
From: from,
|
||||||
|
To: to,
|
||||||
|
Cc: cc,
|
||||||
|
Bcc: bcc,
|
||||||
|
Subject: subject,
|
||||||
|
Body: htmlBody,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(string(raw))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().Int64Var(&id, "id", 0, "existing draft id to send (0 = fresh message)")
|
||||||
|
cmd.Flags().StringVar(&from, "from", "", "from address (default: first enabled mailbox)")
|
||||||
|
cmd.Flags().StringVar(&to, "to", "", "recipient (required unless --id)")
|
||||||
|
cmd.Flags().StringVar(&cc, "cc", "", "cc")
|
||||||
|
cmd.Flags().StringVar(&bcc, "bcc", "", "bcc")
|
||||||
|
cmd.Flags().StringVar(&subject, "subject", "", "subject")
|
||||||
|
cmd.Flags().StringVar(&body, "body", "", "plain text or HTML body")
|
||||||
|
cmd.Flags().StringVar(&html, "html", "", "HTML body (alias of --body when set)")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
func mailsDeleteCmd() *cobra.Command {
|
func mailsDeleteCmd() *cobra.Command {
|
||||||
return &cobra.Command{
|
return &cobra.Command{
|
||||||
Use: "delete ID [ID...]",
|
Use: "delete ID [ID...]",
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/mail"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/mail"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -193,6 +193,49 @@ func (c *Client) SaveMailDraft(ctx context.Context, p SaveMailDraftParams) (map[
|
|||||||
return c.putJSONObject(ctx, "/api/2.0/mail/drafts/save", body)
|
return c.putJSONObject(ctx, "/api/2.0/mail/drafts/save", body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendMailParams describes a message to send via PUT /api/2.0/mail/messages/send.
|
||||||
|
// ID refers to an existing draft/message id; From falls back to the first enabled
|
||||||
|
// mailbox. Cc/Bcc are omitted when empty (the API 400s on empty strings). Chat
|
||||||
|
// line goes into Body (API send does not append the UI signature).
|
||||||
|
type SendMailParams struct {
|
||||||
|
ID int64
|
||||||
|
From string
|
||||||
|
To string
|
||||||
|
Cc string
|
||||||
|
Bcc string
|
||||||
|
Subject string
|
||||||
|
Body string // HTML
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMail sends an existing draft (or a fresh message) via the OnlyOffice Mail
|
||||||
|
// send endpoint. Returns the raw send response.
|
||||||
|
func (c *Client) SendMail(ctx context.Context, p SendMailParams) (json.RawMessage, error) {
|
||||||
|
if strings.TrimSpace(p.To) == "" {
|
||||||
|
return nil, fmt.Errorf("SendMail: 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,
|
||||||
|
"subject": p.Subject,
|
||||||
|
"body": p.Body,
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(p.Cc) != "" {
|
||||||
|
body["cc"] = p.Cc
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(p.Bcc) != "" {
|
||||||
|
body["bcc"] = p.Bcc
|
||||||
|
}
|
||||||
|
return c.putJSON(ctx, "/api/2.0/mail/messages/send.json", body)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) defaultMailFrom(ctx context.Context) (string, error) {
|
func (c *Client) defaultMailFrom(ctx context.Context) (string, error) {
|
||||||
accounts, err := c.ListMailAccounts(ctx)
|
accounts, err := c.ListMailAccounts(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+56
-3
@@ -2,6 +2,7 @@ package onlyoffice
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -49,9 +50,9 @@ func TestMailMessagesPath(t *testing.T) {
|
|||||||
|
|
||||||
func TestParseMailAddress(t *testing.T) {
|
func TestParseMailAddress(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
raw string
|
raw string
|
||||||
wantName string
|
wantName string
|
||||||
wantAddress string
|
wantAddress string
|
||||||
}{
|
}{
|
||||||
{`"LinkedIn Jobbenachrichtigungen" <jobalerts-noreply@linkedin.com>`, "LinkedIn Jobbenachrichtigungen", "jobalerts-noreply@linkedin.com"},
|
{`"LinkedIn Jobbenachrichtigungen" <jobalerts-noreply@linkedin.com>`, "LinkedIn Jobbenachrichtigungen", "jobalerts-noreply@linkedin.com"},
|
||||||
{`"Bitfinex" <no-reply@bitfinex.com>`, "Bitfinex", "no-reply@bitfinex.com"},
|
{`"Bitfinex" <no-reply@bitfinex.com>`, "Bitfinex", "no-reply@bitfinex.com"},
|
||||||
@@ -159,3 +160,55 @@ func TestDownloadMailAttachmentUsesAuthCookie(t *testing.T) {
|
|||||||
t.Fatalf("path = %q", gotPath)
|
t.Fatalf("path = %q", gotPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSendMailOmitsEmptyCcBcc(t *testing.T) {
|
||||||
|
var gotBody map[string]any
|
||||||
|
var gotPath string
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/api/2.0/authentication.json":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"response":{"token":"tok","expires":"2099-01-01T00:00:00.0000000+00:00"}}`))
|
||||||
|
case "/api/2.0/mail/messages/send.json":
|
||||||
|
gotPath = r.URL.Path
|
||||||
|
dec := json.NewDecoder(r.Body)
|
||||||
|
_ = dec.Decode(&gotBody)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"response":{"id":1}}`))
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
c := NewClient(Credentials{Url: srv.URL, User: "u", Password: "p"})
|
||||||
|
ctx := context.Background()
|
||||||
|
raw, err := c.SendMail(ctx, SendMailParams{
|
||||||
|
ID: 99,
|
||||||
|
From: "me@x.com",
|
||||||
|
To: "a@b.com",
|
||||||
|
Subject: "hi",
|
||||||
|
Body: "<p>hello</p>",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SendMail: %v", err)
|
||||||
|
}
|
||||||
|
if gotPath != "/api/2.0/mail/messages/send.json" {
|
||||||
|
t.Fatalf("path = %q", gotPath)
|
||||||
|
}
|
||||||
|
if _, hasCC := gotBody["cc"]; hasCC {
|
||||||
|
t.Fatalf("empty cc should be omitted: %v", gotBody)
|
||||||
|
}
|
||||||
|
if _, hasBcc := gotBody["bcc"]; hasBcc {
|
||||||
|
t.Fatalf("empty bcc should be omitted: %v", gotBody)
|
||||||
|
}
|
||||||
|
if gotBody["to"] != "a@b.com" {
|
||||||
|
t.Fatalf("to = %v", gotBody["to"])
|
||||||
|
}
|
||||||
|
if gotBody["id"] != float64(99) {
|
||||||
|
t.Fatalf("id = %v", gotBody["id"])
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(raw), `"id"`) {
|
||||||
|
t.Fatalf("raw = %s", raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user