Skip to content

Commit e7dc0cf

Browse files
committed
Restructure API docs to auto-generate from OpenAPI spec
- Fetch spec from API server instead of local file - Auto-generate JSON example and field tables from spec - Add allOf/$ref resolution for schema composition - Split into JSON Message and Raw Message sections - Remove "work in progress" disclaimer - Add render_api_example helper for spec-driven examples - Remove html_escape to allow markdown in descriptions
1 parent 9f67c93 commit e7dc0cf

2 files changed

Lines changed: 117 additions & 72 deletions

File tree

content/outbound/sending_email_via_json_api.md

Lines changed: 41 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@ image: http
66

77
# CloudMailin Send Email Message API
88

9-
In order to send an email via API you can create a POST request to the Email
10-
Message endpoint:
9+
To send an email via the API, create a POST request to the Email Message endpoint:
1110

1211
`POST`: `https://api.cloudmailin.com/api/v0.1/{SMTP_USERNAME}/messages`.
1312

14-
Sending email via HTTP POST can be done via one of two methods:
13+
There are several ways to send email:
1514

16-
* If a [client library] is available for your Programming Language / Framework
17-
you can use a client library; Alternatively;
18-
* you can make an HTTP POST to our API manually to send the email
15+
* Use a [client library] if one is available for your language or framework
16+
* Make an HTTP POST with a [JSON message](#json-message) — CloudMailin will construct the email for you
17+
* Make an HTTP POST with a [raw RFC822 message](#raw-message) — useful if you're building the email yourself or migrating from another provider
1918

2019
## Client Libraries
2120

@@ -38,101 +37,61 @@ development.
3837
3938
## Sending Email with an API Call
4039

41-
If your Language / Framework isn't listed above then you can always make a
40+
If your language or framework isn't listed above you can make a
4241
request directly to the Outbound Email API.
4342

44-
You can also use any language / framework via [SMTP].
43+
You can also send email using any language or framework via [SMTP].
4544

4645
### Authentication
4746

48-
Authentication relies on your username and password from you SMTP credentials.
47+
Authentication relies on your username and password from your SMTP credentials.
4948
You can find your SMTP credentials for both live and test accounts on the
5049
[SMTP Accounts] page. Your SMTP username is part of the path used to make the
51-
SMTP request: `POST`:
50+
request: `POST`:
5251
`https://api.cloudmailin.com/api/v0.1/[SMTP_USERNAME]/messages`
5352

5453
You then need to send your SMTP API Token.
5554
Authentication is via the Bearer token in the Authorization header as follows:
5655
`Authorization: Bearer API_TOKEN`.
5756

58-
> This documentation is currently a work in progress. If you need help sending
59-
> via the API please feel free to contact us, alternatively you may wish to use
60-
> [SMTP], which is fully functional.
57+
## JSON Message
6158

62-
## Email Messages Endpoint Example
59+
The simplest way to send email is to POST a JSON object with the message fields.
60+
CloudMailin will construct the email for you.
6361

64-
A full example POST can be seen below:
62+
### Example
6563

6664
```json
67-
{
68-
"from": "Sender Name <sender@example.com>",
69-
"to": [
70-
"Recipient <recipient@example.com>",
71-
"Another <another@example.com>"
72-
],
73-
"test_mode": false,
74-
"subject": "Hello from CloudMailin 😃",
75-
"tags": [
76-
"api-tag",
77-
"cloudmailin-tag"
78-
],
79-
"plain": "Hello Plain Text",
80-
"html": "<h1>Hello Html</h1>",
81-
"headers": {
82-
"x-api-test": "Test",
83-
"x-additional-header": "Value"
84-
},
85-
"attachments": [
86-
{
87-
"file_name": "pixel.png",
88-
"content": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP0rdr1HwAFHwKCk87e6gAAAABJRU5ErkJggg==",
89-
"content_type": "image/png",
90-
"content_id": null
91-
}
92-
]
93-
}
65+
<%= render_api_example("components/schemas/Message") %>
9466
```
9567

96-
Below you can see an explanation of the [fields](#fields), how to add
97-
[attachments](#attachments) and how to set custom [headers](#headers).
98-
9968
### Fields
10069

101-
The API allows sending with the following fields:
102-
10370
| Field | Type | Description |
10471
|---------------|---------|---------------------------------------------------------------------|
105-
<%= render_api_fields("components/schemas/MessageCommon/properties/", include_readonly: false) %>
106-
<%= render_api_fields("components/schemas/Message/allOf/1/properties", 'plain', 'html') %>
72+
<%= render_api_fields("components/schemas/Message", include_readonly: false, except: %w[headers attachments]) %>
10773
| `headers` | object | See the [headers section](#headers)
108-
| `attachments` | Arrary of attachment objects | See the [attachments section](#attachments)
74+
| `attachments` | array of attachment objects | See the [attachments section](#attachments)
10975

11076
### Attachments
11177

112-
Attachments are slightly more complicated and require the following fields
78+
Attachments require the following fields:
11379

11480
| Field | Type | Description |
11581
|---------------|---------|---------------------------------------------------------------------|
116-
<%= render_api_fields("components/schemas/MessageAttachment/properties/") %>
82+
<%= render_api_fields("components/schemas/MessageAttachment") %>
11783

118-
For example this attaches a one-pixel image (Base64 encoded):
84+
For example:
11985

12086
```json
121-
"attachments": [
122-
{
123-
"file_name": "pixel.png",
124-
"content": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP0rdr1HwAFHwKCk87e6gAAAABJRU5ErkJggg==",
125-
"content_type": "image/png",
126-
"content_id": null
127-
}
128-
]
87+
<%= render_api_example("components/schemas/MessageAttachment") %>
12988
```
13089

13190
### Headers
13291

133-
Headers are not required as the subject, to and from headers will be set. However, if you need to
134-
specify additional headers you can pass them as an object.
135-
The key is the header name and the value is expected to be a string a string value:
92+
Headers are not required as the subject, to and from headers will be set automatically.
93+
If you need to specify additional headers you can pass them as an object.
94+
The key is the header name and the value is expected to be a string:
13695

13796
```json
13897
"headers": {
@@ -141,6 +100,24 @@ The key is the header name and the value is expected to be a string a string val
141100
}
142101
```
143102

103+
## Raw Message
104+
105+
If you already have a constructed RFC822 email you can send it directly using
106+
the `raw` field instead of `plain`/`html`. This is useful if you're generating
107+
emails with your own library or migrating from another provider.
108+
109+
### Example
110+
111+
```json
112+
<%= render_api_example("components/schemas/RawMessage") %>
113+
```
114+
115+
### Fields
116+
117+
| Field | Type | Description |
118+
|---------------|---------|---------------------------------------------------------------------|
119+
<%= render_api_fields("components/schemas/RawMessage", include_readonly: false, except: %w[headers attachments]) %>
120+
144121
[Client Library]: #client-libraries
145122
[SMTP Accounts]: https://www.cloudmailin.com/outbound/senders
146123
[SMTP]: <%= url_to_item('/outbound/sending_email_with_smtp/') %>

lib/helpers/openapi_helpers.rb

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
module OpenapiHelpers
22
SPEC_LOCATION = ENV.fetch("API_SPEC_URL", "https://api.cloudmailin.com/api/v0.1")
33

4-
def render_api_fields(section, *fields, include_readonly: true)
5-
all_fields = fetch_api_section(section)
4+
def render_api_fields(section, *fields, include_readonly: true, except: [])
5+
all_fields = resolve_properties(section)
66
fields = all_fields.keys if fields.empty?
77

88
fields.filter_map do |field|
99
content = all_fields[field]
1010
next if content.nil?
11+
next if except.include?(field)
1112
next if !include_readonly && read_only?(content)
1213

1314
render_api_field_content(field, content)
1415
end.join("\n")
1516
end
1617

1718
def render_api_field(section, field)
18-
content = fetch_api_section(section)[field]
19+
content = resolve_properties(section)[field]
1920
render_api_field_content(field, content)
2021
rescue => e
2122
"Error finding #{field} in #{section}: #{e}"
@@ -24,7 +25,7 @@ def render_api_field(section, field)
2425
private
2526

2627
def render_api_field_content(field, content)
27-
description = html_escape(content['description']&.gsub("\n", " ") || "")
28+
description = (content['description'] || "").gsub("\n", " ")
2829
description = "**Response only.** #{description}" if read_only?(content)
2930
type = content['type'] || content['oneOf']&.map { |o|
3031
o['type'] == 'array' ? "array of #{o.dig('items', 'type') || 'items'}" : o['type']
@@ -37,19 +38,86 @@ def read_only?(content)
3738
content['readOnly'] || content['allOf']&.any? { |item| item['readOnly'] }
3839
end
3940

41+
def render_api_example(section, except: [])
42+
yaml = YAML.load(fetch_spec)
43+
schema = fetch_section(section)
44+
props = collect_properties(schema)
45+
example = {}
46+
47+
props.each do |k, v|
48+
next if v['readOnly']
49+
next if except.include?(k)
50+
51+
if v['oneOf']
52+
ex = v['oneOf'].first['example']
53+
example[k] = ex if ex
54+
elsif v['items'] && v['items']['$ref']
55+
ref_schema = resolve_ref(yaml, v['items']['$ref'])
56+
item_example = build_example(ref_schema)
57+
example[k] = [item_example] unless item_example.empty?
58+
elsif !v['example'].nil?
59+
example[k] = v['example']
60+
end
61+
end
62+
63+
JSON.pretty_generate(example)
64+
end
65+
66+
def resolve_ref(yaml, ref)
67+
path = ref.delete_prefix('#/').split('/')
68+
yaml.dig(*path) || {}
69+
end
70+
71+
def build_example(schema)
72+
props = schema['properties'] || {}
73+
example = {}
74+
props.each do |k, v|
75+
example[k] = v['example'] unless v['example'].nil?
76+
end
77+
example
78+
end
79+
4080
def render_api_key(section, field)
41-
"`#{field}` | #{fetch_api_section(section)&.dig(field).inspect}"
81+
"`#{field}` | #{resolve_properties(section)&.dig(field).inspect}"
4282
end
4383

4484
protected
4585

46-
def fetch_api_section(section_name)
86+
# Resolves a schema section to a flat hash of properties.
87+
# Handles $ref, allOf, and direct properties.
88+
def resolve_properties(section)
89+
schema = fetch_section(section)
90+
return schema if schema.nil?
91+
92+
# If we're already looking at a properties hash (no type/allOf/$ref)
93+
return schema unless schema.is_a?(Hash) && (schema['allOf'] || schema['$ref'] || schema['properties'])
94+
95+
collect_properties(schema)
96+
end
97+
98+
def collect_properties(schema)
99+
return {} unless schema.is_a?(Hash)
100+
101+
if schema['$ref']
102+
ref_path = schema['$ref'].delete_prefix('#/').split('/')
103+
resolved = fetch_section(ref_path.join('/'))
104+
return collect_properties(resolved)
105+
end
106+
107+
props = {}
108+
if schema['allOf']
109+
schema['allOf'].each { |entry| props.merge!(collect_properties(entry)) }
110+
end
111+
props.merge!(schema['properties']) if schema['properties']
112+
props
113+
end
114+
115+
def fetch_section(section_name)
47116
yaml = YAML.load(fetch_spec)
48117
section_array = section_name.split('/')
49118
section_array.map! { |i| i.match?(/\d+/) ? i.to_i : i }
50119

51-
hash = yaml.to_h
52-
hash.dig(*section_array)
120+
yaml.to_h.dig(*section_array)
53121
end
54122

55123
def fetch_spec

0 commit comments

Comments
 (0)