13// Client can be used to call webapi methods.
14// Client implements [Methods].
16 BaseURL string // For example: http://localhost:1080/webapi/v0/.
17 Username string // Added as HTTP basic authentication if not empty.
19 HTTPClient *http.Client // Optional, defaults to http.DefaultClient.
22var _ Methods = Client{}
24func (c Client) httpClient() *http.Client {
25 if c.HTTPClient != nil {
28 return http.DefaultClient
31func transact[T any](ctx context.Context, c Client, fn string, req any) (resp T, rerr error) {
32 hresp, err := httpDo(ctx, c, fn, req)
36 defer hresp.Body.Close()
38 if hresp.StatusCode == http.StatusOK {
39 // Text and HTML of a message can each be 1MB. Another MB for other data would be a
41 err := json.NewDecoder(&limitReader{hresp.Body, 3 * 1024 * 1024}).Decode(&resp)
44 return resp, badResponse(hresp)
47func transactReadCloser(ctx context.Context, c Client, fn string, req any) (resp io.ReadCloser, rerr error) {
48 hresp, err := httpDo(ctx, c, fn, req)
58 if hresp.StatusCode == http.StatusOK {
63 return nil, badResponse(hresp)
66func httpDo(ctx context.Context, c Client, fn string, req any) (*http.Response, error) {
67 reqbuf, err := json.Marshal(req)
69 return nil, fmt.Errorf("marshal request: %v", err)
72 data.Add("request", string(reqbuf))
73 hreq, err := http.NewRequestWithContext(ctx, "POST", c.BaseURL+fn, strings.NewReader(data.Encode()))
75 return nil, fmt.Errorf("new request: %v", err)
77 hreq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
79 hreq.SetBasicAuth(c.Username, c.Password)
81 hresp, err := c.httpClient().Do(hreq)
83 return nil, fmt.Errorf("http transaction: %v", err)
88func badResponse(hresp *http.Response) error {
89 if hresp.StatusCode != http.StatusBadRequest {
90 return fmt.Errorf("http status %v, expected 200 ok", hresp.Status)
92 buf, err := io.ReadAll(&limitReader{R: hresp.Body, Limit: 10 * 1024})
94 return fmt.Errorf("reading error from remote: %v", err)
97 err = json.Unmarshal(buf, &xerr)
102 return fmt.Errorf("error parsing error from remote: %v (first 512 bytes of response: %s)", err, string(buf))
107// Send composes a message and submits it to the queue for delivery for all
108// recipients (to, cc, bcc).
110// Configure your account to use unique SMTP MAIL FROM addresses ("fromid") and to
111// keep history of retired messages, for better handling of transactional email,
112// automatically managing a suppression list.
114// Configure webhooks to receive updates about deliveries.
116// If the request is a multipart/form-data, uploaded files with the form keys
117// "alternativefile", "inlinefile" and/or "attachedfile" will be added to the
118// message. If the uploaded file has content-type and/or content-id headers, they
119// will be included. If no content-type is present in the request, and it can be
120// detected, it is included automatically.
122// Example call with a text and html message, with an inline and an attached image:
124// curl --user mox@localhost:moxmoxmox \
125// --form request='{"To": [{"Address": "mox@localhost"}], "Text": "hi ☺", "HTML": "<img src=\"cid:hi\" />"}' \
126// --form 'inlinefile=@hi.png;headers="Content-ID: <hi>"' \
127// --form attachedfile=@mox.png \
128// http://localhost:1080/webapi/v0/Send
132// - badAddress, if an email address is invalid.
133// - missingBody, if no text and no html body was specified.
134// - multipleFrom, if multiple from addresses were specified.
135// - badFrom, if a from address was specified that isn't configured for the account.
136// - noRecipients, if no recipients were specified.
137// - messageLimitReached, if the outgoing message rate limit was reached.
138// - recipientLimitReached, if the outgoing new recipient rate limit was reached.
139// - messageTooLarge, message larger than configured maximum size.
140// - malformedMessageID, if MessageID is specified but invalid.
141// - sentOverQuota, message submitted, but not stored in Sent mailbox due to quota reached.
142func (c Client) Send(ctx context.Context, req SendRequest) (resp SendResult, err error) {
143 return transact[SendResult](ctx, c, "Send", req)
146// SuppressionList returns the addresses on the per-account suppression list.
147func (c Client) SuppressionList(ctx context.Context, req SuppressionListRequest) (resp SuppressionListResult, err error) {
148 return transact[SuppressionListResult](ctx, c, "SuppressionList", req)
151// SuppressionAdd adds an address to the suppression list of the account.
155// - badAddress, if the email address is invalid.
156func (c Client) SuppressionAdd(ctx context.Context, req SuppressionAddRequest) (resp SuppressionAddResult, err error) {
157 return transact[SuppressionAddResult](ctx, c, "SuppressionAdd", req)
160// SuppressionRemove removes an address from the suppression list of the account.
164// - badAddress, if the email address is invalid.
165func (c Client) SuppressionRemove(ctx context.Context, req SuppressionRemoveRequest) (resp SuppressionRemoveResult, err error) {
166 return transact[SuppressionRemoveResult](ctx, c, "SuppressionRemove", req)
169// SuppressionPresent returns whether an address is present in the suppression list of the account.
173// - badAddress, if the email address is invalid.
174func (c Client) SuppressionPresent(ctx context.Context, req SuppressionPresentRequest) (resp SuppressionPresentResult, err error) {
175 return transact[SuppressionPresentResult](ctx, c, "SuppressionPresent", req)
178// MessageGet returns a message from the account storage in parsed form.
180// Use [Client.MessageRawGet] for the raw message (internet message file).
183// - messageNotFound, if the message does not exist.
184func (c Client) MessageGet(ctx context.Context, req MessageGetRequest) (resp MessageGetResult, err error) {
185 return transact[MessageGetResult](ctx, c, "MessageGet", req)
188// MessageRawGet returns the full message in its original form, as stored on disk.
191// - messageNotFound, if the message does not exist.
192func (c Client) MessageRawGet(ctx context.Context, req MessageRawGetRequest) (resp io.ReadCloser, err error) {
193 return transactReadCloser(ctx, c, "MessageRawGet", req)
196// MessagePartGet returns a single part from a multipart message, by a "parts
197// path", a series of indices into the multipart hierarchy as seen in the parsed
198// message. The initial selection is the body of the outer message (excluding
202// - messageNotFound, if the message does not exist.
203// - partNotFound, if the part does not exist.
204func (c Client) MessagePartGet(ctx context.Context, req MessagePartGetRequest) (resp io.ReadCloser, err error) {
205 return transactReadCloser(ctx, c, "MessagePartGet", req)
208// MessageDelete permanently removes a message from the account storage (not moving
209// to a Trash folder).
212// - messageNotFound, if the message does not exist.
213func (c Client) MessageDelete(ctx context.Context, req MessageDeleteRequest) (resp MessageDeleteResult, err error) {
214 return transact[MessageDeleteResult](ctx, c, "MessageDelete", req)
217// MessageFlagsAdd adds (sets) flags on a message, like the well-known flags
218// beginning with a backslash like \seen, \answered, \draft, or well-known flags
219// beginning with a dollar like $junk, $notjunk, $forwarded, or custom flags.
220// Existing flags are left unchanged.
223// - messageNotFound, if the message does not exist.
224func (c Client) MessageFlagsAdd(ctx context.Context, req MessageFlagsAddRequest) (resp MessageFlagsAddResult, err error) {
225 return transact[MessageFlagsAddResult](ctx, c, "MessageFlagsAdd", req)
228// MessageFlagsRemove removes (clears) flags on a message.
229// Other flags are left unchanged.
232// - messageNotFound, if the message does not exist.
233func (c Client) MessageFlagsRemove(ctx context.Context, req MessageFlagsRemoveRequest) (resp MessageFlagsRemoveResult, err error) {
234 return transact[MessageFlagsRemoveResult](ctx, c, "MessageFlagsRemove", req)
237// MessageMove moves a message to a new mailbox name (folder). The destination
238// mailbox name must already exist.
241// - messageNotFound, if the message does not exist.
242func (c Client) MessageMove(ctx context.Context, req MessageMoveRequest) (resp MessageMoveResult, err error) {
243 return transact[MessageMoveResult](ctx, c, "MessageMove", req)