{
  "components": {
    "parameters": {
      "id": {
        "in": "path",
        "name": "id",
        "required": true,
        "schema": {
          "format": "uuid",
          "type": "string"
        }
      }
    },
    "responses": {
      "BadQuery": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        },
        "description": "A query parameter could not be read."
      },
      "Conflict": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        },
        "description": "Something else already claims that username or sender pattern."
      },
      "Invalid": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        },
        "description": "The body was read but refused."
      },
      "NotFound": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        },
        "description": "There is nothing here."
      },
      "ReadOnly": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        },
        "description": "This key may read but not change anything."
      },
      "Unauthorized": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        },
        "description": "No key, or one that is unknown, revoked or expired."
      }
    },
    "schemas": {
      "BodyCapture": {
        "description": "How much of a message is kept. An application asks for a level and the\ndestination carrying it caps that, never raises it.\n",
        "enum": [
          "none",
          "headers",
          "full"
        ],
        "type": "string"
      },
      "Inbound": {
        "properties": {
          "allowedNetworks": {
            "description": "Addresses or CIDR blocks it is recognised from, without signing in.\nThis replaces authenticating; it does not restrict where the\npassword may be used from.\n",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "allowedSenders": {
            "description": "Envelope senders it may use: an address, `*@domain`, or `*`.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "bodyCapture": {
            "$ref": "#/components/schemas/BodyCapture"
          },
          "createdAt": {
            "format": "date-time",
            "type": "string"
          },
          "enabled": {
            "type": "boolean"
          },
          "hasPassword": {
            "description": "The password itself is never returned.",
            "type": "boolean"
          },
          "id": {
            "format": "uuid",
            "type": "string"
          },
          "lastUsedAt": {
            "format": "date-time",
            "nullable": true,
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "pinnedOutboundId": {
            "description": "Always send this way, whatever the sender.",
            "format": "uuid",
            "nullable": true,
            "type": "string"
          },
          "updatedAt": {
            "format": "date-time",
            "type": "string"
          },
          "username": {
            "description": "What it signs in with.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "InboundPage": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/Inbound"
            },
            "type": "array"
          },
          "total": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "InboundWrite": {
        "properties": {
          "allowedNetworks": {
            "description": "May not be `0.0.0.0/0` — recognised without a password from anywhere is an open relay.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "allowedSenders": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "bodyCapture": {
            "$ref": "#/components/schemas/BodyCapture"
          },
          "enabled": {
            "type": "boolean"
          },
          "name": {
            "type": "string"
          },
          "password": {
            "description": "At least 12 characters. May be omitted for an application that names\nat least one network, which is then recognised without signing in.\n",
            "type": "string"
          },
          "pinnedOutboundId": {
            "format": "uuid",
            "nullable": true,
            "type": "string"
          },
          "username": {
            "type": "string"
          }
        },
        "required": [
          "name",
          "username"
        ],
        "type": "object"
      },
      "Message": {
        "properties": {
          "capture": {
            "$ref": "#/components/schemas/BodyCapture"
          },
          "envelopeFrom": {
            "type": "string"
          },
          "error": {
            "type": "string"
          },
          "id": {
            "format": "uuid",
            "type": "string"
          },
          "inboundId": {
            "format": "uuid",
            "nullable": true,
            "type": "string"
          },
          "messageId": {
            "type": "string"
          },
          "outboundId": {
            "format": "uuid",
            "nullable": true,
            "type": "string"
          },
          "outboundName": {
            "description": "Kept even after the destination is removed.",
            "type": "string"
          },
          "receivedAt": {
            "format": "date-time",
            "type": "string"
          },
          "recipients": {
            "items": {
              "$ref": "#/components/schemas/Recipient"
            },
            "type": "array"
          },
          "relayedAt": {
            "format": "date-time",
            "nullable": true,
            "type": "string"
          },
          "remoteAddr": {
            "type": "string"
          },
          "sizeBytes": {
            "format": "int64",
            "type": "integer"
          },
          "status": {
            "description": "`relayed` means the upstream accepted it, which is as far as this\nproxy can see. `rejected` means letterproxy refused it; `failed`\nmeans the upstream did, or could not be reached.\n",
            "enum": [
              "receiving",
              "relayed",
              "rejected",
              "failed"
            ],
            "type": "string"
          },
          "subject": {
            "type": "string"
          },
          "upstream": {
            "properties": {
              "code": {
                "type": "integer"
              },
              "queueId": {
                "description": "The upstream's own identifier, where its reply offered one. The handle for its logs.",
                "type": "string"
              },
              "response": {
                "type": "string"
              }
            },
            "type": "object"
          },
          "username": {
            "description": "Kept even after the application is removed.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "MessagePage": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/Message"
            },
            "type": "array"
          },
          "page": {
            "type": "integer"
          },
          "pageSize": {
            "type": "integer"
          },
          "total": {
            "description": "How many the filter matched, not how many are on this page.",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "Outbound": {
        "properties": {
          "createdAt": {
            "format": "date-time",
            "type": "string"
          },
          "enabled": {
            "type": "boolean"
          },
          "id": {
            "format": "uuid",
            "type": "string"
          },
          "matchHeaderFrom": {
            "description": "Resolve again on the message's `From:` header once it has arrived.",
            "type": "boolean"
          },
          "maxBodyCapture": {
            "allOf": [
              {
                "$ref": "#/components/schemas/BodyCapture"
              }
            ],
            "description": "A ceiling on what an application sending this way may keep."
          },
          "name": {
            "type": "string"
          },
          "pattern": {
            "description": "The senders this claims: an address, `*@domain`, or `*`.",
            "type": "string"
          },
          "priority": {
            "description": "Lower first, when two patterns are equally narrow.",
            "type": "integer"
          },
          "reaches": {
            "description": "How broadly the pattern selects. The narrowest match wins.",
            "enum": [
              "exact",
              "domain",
              "any"
            ],
            "type": "string"
          },
          "updatedAt": {
            "format": "date-time",
            "type": "string"
          },
          "upstream": {
            "$ref": "#/components/schemas/Upstream"
          }
        },
        "type": "object"
      },
      "OutboundPage": {
        "properties": {
          "items": {
            "items": {
              "$ref": "#/components/schemas/Outbound"
            },
            "type": "array"
          },
          "total": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "OutboundWrite": {
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "matchHeaderFrom": {
            "type": "boolean"
          },
          "maxBodyCapture": {
            "$ref": "#/components/schemas/BodyCapture"
          },
          "name": {
            "type": "string"
          },
          "pattern": {
            "type": "string"
          },
          "priority": {
            "type": "integer"
          },
          "upstream": {
            "properties": {
              "host": {
                "type": "string"
              },
              "password": {
                "description": "Stored as given, because the proxy has to present it. Never returned.",
                "type": "string"
              },
              "port": {
                "maximum": 65535,
                "minimum": 1,
                "type": "integer"
              },
              "security": {
                "default": "starttls",
                "enum": [
                  "none",
                  "starttls",
                  "tls"
                ],
                "type": "string"
              },
              "username": {
                "type": "string"
              }
            },
            "required": [
              "host",
              "port"
            ],
            "type": "object"
          }
        },
        "required": [
          "name",
          "pattern",
          "upstream"
        ],
        "type": "object"
      },
      "Problem": {
        "properties": {
          "detail": {
            "description": "What went wrong, for a person.",
            "type": "string"
          },
          "error": {
            "description": "A short, stable code.",
            "example": "invalid",
            "type": "string"
          }
        },
        "type": "object"
      },
      "Recipient": {
        "properties": {
          "accepted": {
            "type": "boolean"
          },
          "address": {
            "type": "string"
          },
          "code": {
            "description": "What the upstream told this recipient. An upstream can accept some and refuse others.",
            "type": "integer"
          },
          "response": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "Upstream": {
        "properties": {
          "hasPassword": {
            "type": "boolean"
          },
          "host": {
            "type": "string"
          },
          "port": {
            "type": "integer"
          },
          "security": {
            "enum": [
              "none",
              "starttls",
              "tls"
            ],
            "type": "string"
          },
          "username": {
            "type": "string"
          }
        },
        "type": "object"
      }
    },
    "securitySchemes": {
      "apiKey": {
        "description": "A key issued under Settings → API keys, sent as `Authorization: Bearer lp_…`.",
        "scheme": "bearer",
        "type": "http"
      }
    }
  },
  "info": {
    "description": "The administrative API: the inbound and outbound configuration the console\nedits, and the message log it reads.\n\nEverything here has an equivalent in the console at `/a/`, and nothing here\ndoes anything the console cannot. What a message kept of itself is\ndeliberately absent — stored content is read through the console, where\nlooking at it is written to the audit log.\n\nAuthenticate with a key issued under Settings → API keys. A key is shown\nonce, when it is issued, and only a hash of it is stored. A read-only key\nmay call every `GET` and nothing else.\n",
    "title": "letterproxy",
    "version": "1.0"
  },
  "openapi": "3.0.3",
  "paths": {
    "/inbound": {
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InboundPage"
                }
              }
            },
            "description": "Every application."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "summary": "List applications",
        "tags": [
          "inbound"
        ]
      },
      "post": {
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InboundWrite"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Inbound"
                }
              }
            },
            "description": "Added."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ReadOnly"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          }
        },
        "summary": "Add an application",
        "tags": [
          "inbound"
        ]
      }
    },
    "/inbound/{id}": {
      "delete": {
        "description": "Messages it sent keep their history and their username.",
        "responses": {
          "204": {
            "description": "Removed."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ReadOnly"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "summary": "Remove an application",
        "tags": [
          "inbound"
        ]
      },
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Inbound"
                }
              }
            },
            "description": "The application."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "summary": "Read one application",
        "tags": [
          "inbound"
        ]
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "put": {
        "description": "Omitting `password` keeps the stored one. Sending an empty string does\nthe same: an update is not a way to blank a credential by forgetting to\nmention it.\n",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InboundWrite"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Inbound"
                }
              }
            },
            "description": "Saved."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ReadOnly"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          }
        },
        "summary": "Replace an application",
        "tags": [
          "inbound"
        ]
      }
    },
    "/messages": {
      "get": {
        "description": "Newest first.\n\n`recipients` is empty here: reading them costs a query per message, so\nthey are carried by `GET /messages/{id}` instead.\n",
        "parameters": [
          {
            "description": "Envelope sender, matched exactly and case-insensitively.",
            "in": "query",
            "name": "from",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "One envelope recipient, matched exactly.",
            "in": "query",
            "name": "to",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Text in the subject, sender or Message-ID. Matched literally — a percent sign is a percent sign.",
            "in": "query",
            "name": "q",
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "status",
            "schema": {
              "enum": [
                "receiving",
                "relayed",
                "rejected",
                "failed"
              ],
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "inboundId",
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "outboundId",
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "since",
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "until",
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "page",
            "schema": {
              "default": 1,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "Capped at 200 rather than refused.",
            "in": "query",
            "name": "pageSize",
            "schema": {
              "default": 50,
              "maximum": 200,
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessagePage"
                }
              }
            },
            "description": "A page of the log."
          },
          "400": {
            "$ref": "#/components/responses/BadQuery"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "summary": "List messages",
        "tags": [
          "messages"
        ]
      }
    },
    "/messages/{id}": {
      "get": {
        "description": "With its recipients and what each was told.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Message"
                }
              }
            },
            "description": "The message."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "summary": "Read one message",
        "tags": [
          "messages"
        ]
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ]
    },
    "/outbound": {
      "get": {
        "description": "In the order the proxy resolves them.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OutboundPage"
                }
              }
            },
            "description": "Every destination."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "summary": "List destinations",
        "tags": [
          "outbound"
        ]
      },
      "post": {
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OutboundWrite"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Outbound"
                }
              }
            },
            "description": "Added."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ReadOnly"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          }
        },
        "summary": "Add a destination",
        "tags": [
          "outbound"
        ]
      }
    },
    "/outbound/{id}": {
      "delete": {
        "description": "Messages it carried keep their history and its name.",
        "responses": {
          "204": {
            "description": "Removed."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ReadOnly"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "summary": "Remove a destination",
        "tags": [
          "outbound"
        ]
      },
      "get": {
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Outbound"
                }
              }
            },
            "description": "The destination."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "summary": "Read one destination",
        "tags": [
          "outbound"
        ]
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "put": {
        "description": "Omitting `upstream.password` keeps the stored one. It is never returned,\nbecause the proxy has to present it to the upstream and so cannot hash\nit.\n",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OutboundWrite"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Outbound"
                }
              }
            },
            "description": "Saved."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ReadOnly"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Invalid"
          }
        },
        "summary": "Replace a destination",
        "tags": [
          "outbound"
        ]
      }
    }
  },
  "security": [
    {
      "apiKey": []
    }
  ],
  "servers": [
    {
      "url": "https://letters.k15g.no/api/v1"
    }
  ],
  "tags": [
    {
      "description": "The applications allowed to send, and what each may send as.",
      "name": "inbound"
    },
    {
      "description": "Where mail goes, chosen by who sent it.",
      "name": "outbound"
    },
    {
      "description": "What the proxy carried.",
      "name": "messages"
    }
  ]
}