# Meta information
meta:
  # Document name and ID
  id: csp
  name: Chat Server Protocol

  # References used by the structs
  references:
    # A public or secret key
    key: &key b32
    # A random cookie
    cookie: &cookie b16
    # A random nonce
    nonce: &nonce b24
    # A Threema ID
    identity: &identity b8
    # Multiple Threema IDs
    identities: &identities b8[]
    # A message ID
    message-id: &message-id u64-le
    # Multiple message IDs
    message-ids: &message-ids u64-le[]
    # A blob ID
    blob-id: &blob-id b16
    # A poll ID
    poll-id: &poll-id u64-le
    # A group ID
    group-id: &group-id u64-le

# Virtual namespace, just containing the below docstring
index: &index
  _doc: |-
    # Chat Server Protocol

    The Chat Server Protocol is a custom transport encrypted frame-based
    protocol, originally designed to operate on top of TCP. it uses the NaCl
    cryptography library to provide authentication, integrity and encryption.

    The login [**handshake**](ref:handshake) takes two round trips and
    establishes ephemeral encryption keys along the way. Authentication is
    solely based on the secret key associated to a Threema ID.

    After the handshake process, [**payloads**](ref:payload) can be exchanged
    bidirectionally although some payload structs may only be used in one
    direction. A client may now send and receive end-to-end encrypted
    [**messages**](ref:e2e) (wrapped in [message payload
    structs](ref:payload.container)).

    ## Terminology

    - `CK`: Client Key (permanent secret key associated to the Threema ID)
    - `SK`: Permanent Server Key
    - `TCK`: Temporary Client Key
    - `TSK`: Temporary Server Key
    - `CCK`: Client Connection Cookie
    - `SCK`: Server Connection Cookie
    - `CSN`: Client Sequence Number
    - `SSN`: Server Sequence Number
    - `ID`: The client's Threema ID

    ## General Information

    **Endianness:** All integers use little-endian encoding.

    **Encryption cipher:** XSalsa20-Poly1305, unless otherwise specified.

    **Nonce format:**

    - a 16 byte cookie (CCK/SCK), followed by
    - a monotonically increasing sequence number (CSN/SSN, u64-le).

    **Sequence number:** The sequence number starts with `1` and is counted
    separately for each direction (i.e. there is one sequence number counter for
    the client and one for the server). We will use `CSN+` and `SSN+` in this
    document to denote that the counter should be increased **after** the value
    has been inserted (i.e. semantically equivalent to `x++` in many languages).

    ## Size Limitations

    The chat server protocol currently allows for up to 8192 bytes within a
    single frame. Because we make heavy use of Protobuf messages, the overhead
    cannot be calculated reliably ahead of time. Therefore, the total amount of
    user-defined bytes should be constrained to ~7000 bytes. To achieve this,
    the maximum recommended size of each property will be defined for each
    message, so that it's total size roughly matches that constraint.

# Handshake structs
handshake: &handshake
  _doc: |-
    ## Handshake

    To perform authentication handshake, the following handshake structs have to
    be exchanged in this order:

        C -- client-hello -> S
        C <- server-hello -- S
        C ---- login ---- -> S
        C <-- login-ack ---- S

    Note that handshake structs have no wrapping frame container struct.

  client-hello:
    _doc: |-
      Initial message from the client, containing a server authentication
      challenge in order to establish transport layer encryption.

      Direction: Client --> Server
    fields:
      - _doc: |-
          32 byte temporary public key (`TCK.public`).
        name: tck
        type: *key
      - _doc: |-
          16 byte random cookie used for nonces (also acting as server
          authentication challenge).
        name: cck
        type: *cookie

  server-hello:
    _doc: |-
      Initial message from the server, containing the server's authentication
      challenge response. This concludes establishing transport layer
      encryption based on `TCK` and `TSK`.

      Direction: Client <-- Server

      When creating this message:

      1. Ensure that CCK and SCK are not equal.

      When receiving this message:

      1. If CCK and SCK are equal, abort the connection and these steps.
      2. If the repeated random cookie of the client does not equal CCK,
         abort the connection and these steps.
    fields:
      - _doc: |-
          16 byte random cookie used for nonces (also acting as client
          authentication challenge)
        name: sck
        type: *cookie
      - _doc: |-
          The server's challenge response (`server-challenge-response`),
          encrypted by:

              XSalsa20-Poly1305(
                key=X25519HSalsa20(SK.secret, TCK.public),
                nonce=SCK || u64-le(SSN+),
              )
        name: server-challenge-response-box
        type: b64

  server-challenge-response:
    _doc: |-
      Authentication challenge response from the server.
    fields:
      - _doc: |-
          32 byte temporary public key (`TSK.public`)
        name: tsk
        type: *key
      - _doc: |-
          16 byte repeated random cookie of the client (acting as the server's
          challenge response)
        name: cck
        type: *cookie

  login:
    _doc: |-
      Login request from the client.

      IMPORTANT: `CSN` is used and increased for `box` and then for
      `extension-box`. It must follow this exact order.

      Direction: Client --> Server
    fields:
      - _doc: |-
          The [`login-data`](ref:handshake.login-data), encrypted by:

              XSalsa20-Poly1305(
                key=X25519HSalsa20(TCK.secret, TSK.public),
                nonce=CCK || u64-le(CSN+),
              )
        name: box
        type: b144
      - _doc: |-
          An optional arbitrary amount of
          [`extension`](ref:handshake.extension)s, encrypted by:

              XSalsa20-Poly1305(
                key=X25519HSalsa20(TCK.secret, TSK.public),
                nonce=CCK || u64-le(CSN+),
              )

          These fields are only present if the
          [`extension-indicator`](ref:handshake.extension-indicator) of the
          [`login-data`](ref:handshake.login-data) field is present. If so,
          extensions should be consumed until the extension indicator `length`
          field is zero.
        name: extensions-box
        type: b*

  login-data:
    _doc: |-
      Login data of the client.
    fields:
      - _doc: |-
          Threema ID of the client.
        name: identity
        type: *identity
      - _doc: |-
          This is either the old client info field or an extension indicator.

          If the first 30 bytes of the field start with the string
          `threema-clever-extension-field`, then parse this field as an
          [`extension-indicator`](ref:handshake.extension-indicator) and parse
          `extensions-box` appropriately.

          Otherwise, this represents an old client info and the content is
          identical to the content of
          [`client-info`](ref:handshake.client-info). Since the field has a
          fixed size, the string is zero-padded.
        name: client-info-or-extension-indicator
        type: b32
      - _doc: |-
          16 byte repeated random cookie of the server (acting as the client's
          challenge response)
        name: sck
        type: *cookie
      - _doc: |-
          24 zero bytes (previously used as vouch nonce, now set to zero
          indicating that the new vouch format is being used)
        name: reserved1
        type: b24
      - _doc: |-
          The vouch value, calculated as follows:

              SS1 = X25519HSalsa20(CK.secret, SK.public)
              SS2 = X25519HSalsa20(CK.secret, TSK.public)
              VouchKey = BLAKE2b(key=SS1 || SS2, salt='v2', personal='3ma-csp')
              vouch = BLAKE2b(
                out-length=32,
                key=VouchKey,
                input=SCK || TCK.public,
              )

        name: vouch
        type: b32
      - _doc: |-
          16 zero bytes (previously part of the vouch box, now set to zero for
          compatibility)
        name: reserved2
        type: b16

  extension-indicator:
    _doc: |-
      Indicates that extensions are present
    fields:
      - _doc: |-
          Magic string: `threema-clever-extension-field`
        name: magic
        type: b30
      - _doc: |-
          Amount of encrypted bytes present for extensions. Extension fields
          need to be consumed until `length` is zero.
        name: length
        type: u16-le

  extension:
    _doc: |-
      An extension field.
    fields:
      - _doc: |-
          Type of the extension. Must correspond to the encoded extension struct
          of the `payload` field:

          - `0x00`: `client-info`
          - `0x01`: `csp-device-id`
          - `0x02`: `message-payload-version`
          - `0x03`: `device-cookie`
        name: type
        type: u8
      - _doc: |-
          Length of the extension's `payload` field.
        name: length
        type: u16-le
      - _doc: |-
          Extension payload. Needs to be parsed according to the `type` field.
        name: payload
        type: b{length}

  client-info:
    _doc: |-
      Client info extension payload.
    fields:
      - _doc: |-
          Client info string in the following format (without line breaks):

              <app-version>;
              <platform>;
              <lang>/<country-code>;
              <rest>

          The `<rest>` looks like this for mobile clients (A/I/W):

              <device-model>;
              <os-version>

          The `<rest>` looks like this for web/desktop clients (Q):

              <renderer>;
              <renderer-version>;
              <os-name>;
              <os-architecture>

          The `<rest>` looks like this for Bots (B):

              <os-name>;
              <os-architecture>

          The fields may contain the following values:

          - `app-version`: Arbitrary version string, depending on the platform
          - `platform`:
            * `A`: Android
            * `I`: iOS
            * `Q`: Desktop/Web
            * `W`: Windows Phone
            * `B`: Bot
          - `lang`: ISO 639-1:2002-ish language code
          - `country-code`: ISO 3166-1-ish country code
          - `device-model`: Arbitrary smartphone model
          - `os-version`: Arbitrary OS version string
          - `renderer`: Renderer name for Desktop/Web (e.g. `Firefox` or
            `Electron`)
          - `renderer-version`: Renderer major version (e.g. `107`)
          - `os-name`: Name of the operating system (e.g. `Linux` or `Windows`)
          - `os-architecture`: Architecture of the operating system (e.g. `x64`)
        name: client-info
        type: b*

  csp-device-id:
    _doc: |-
      CSP device ID extension payload.
    fields:
      - _doc: |-
          CSP device ID, randomly generated **once** when the device got the
          Mediator device ID.
        name: csp-device-id
        type: u64-le

  message-payload-version:
    _doc: |-
      Message payload struct version to be used.

      In case this extension is not present, the server must assume that
      version `0x00` has been selected.

      In case the server receives an unknown or unsupported protocol version,
      it shall complete the handshake and then immediately send a `close-error`
      payload.
    fields:
      - _doc: |-
          Indicates the payload struct version the client will send and expects
          to receive when exchanging message payload structs with the server:

          - `0x00`: `legacy-message`
          - `0x01`: `message-with-metadata-box`
        name: version
        type: u8

  device-cookie:
    _doc: |-
      A 16 byte random value chosen by the client and stored in a secure,
      device-specific location (not included in any backups etc., not
      viewable/exportable).

      Its purpose is to allow detection when a different (rogue) device has
      connected to the chat server, e.g. because an attacker has obtained
      the secret key of a user.

      The server will store the device cookie of the last connection, and if a
      different cookie is sent by the client, it will set a flag on the identity
      and send a
      [`device-cookie-change-indication`](ref:payload.device-cookie-change-indication)
      payload to the client every time it connects. The client should then show
      a warning in form of a notification or a dialog to the user. Note that the
      normal protocol flow should continue regardless of whether the user has
      acknowledged the warning or not.

      If this extension is not sent by the client, then the server's behavior
      depends on whether it has already stored a device cookie for this
      identity or not. If not, then nothing will happen. If yes, then it will
      act as if the client had sent an all-zero device cookie.
    fields:
      - _doc: |-
          Device cookie, randomly generated **once** per device.
        name: device-cookie
        type: b16

  login-ack:
    _doc: |-
      Login acknowledgement from the server.

      Direction: Client <-- Server
    fields:
      - _doc: |-
          Reserved (16 zero bytes), encrypted by:

              XSalsa20-Poly1305(
                key=X25519HSalsa20(TSK.secret, TCK.public),
                nonce=SCK || u64-le(SSN+),
              )
        name: reserved-box
        type: b32

# Payload structs
payload: &payload
  _doc: |-
    ## Payload

    After the handshake process, payloads may be sent and received without any
    strictly defined order.

    Note that payload structs are mandatory to encrypt and frame. To achieve
    this, first wrap the payload struct in a
    [`container`](ref:payload.container) struct, encrypt it and wrap the
    encrypted bytes in a [`frame`](ref:payload.frame) struct.

  frame:
    _group: Header
    _doc: |-
      Contains an encrypted [payload](ref:payload#payload) wrapped in a
      [container](ref:payload.container).

      Direction: Client <-> Server
    fields:
      - _doc: |-
          Length of the `box` field.
        name: length
        type: u16-le
      - _doc: |-
          The encrypted [payload](ref:payload#payload).

          For messages from the server to the client, encrypted by:

              XSalsa20-Poly1305(
                key=X25519HSalsa20(TSK.secret, TCK.public),
                nonce=SCK || u64-le(SSN+),
              )

          For messages from the  client to the server, encrypted by:

              XSalsa20-Poly1305(
                key=X25519HSalsa20(TSK.secret, TCK.public),
                nonce=CCK || u64-le(CSN+),
              )
        name: box
        type: b{length}

  container:
    _group: Header
    _doc: |-
      Contains an inner [payload](ref:payload#payload) struct.

      Direction: Client <-> Server
    fields:
      - _doc: |-
          Type of the payload. Must correspond to the encoded payload struct
          of the `data` field:

          - `0x00`: [`echo-request`](ref:payload.echo-request)
          - `0x80`: [`echo-response`](ref:payload.echo-response)
          - `0x01`: outgoing [`legacy-message`](ref:payload.legacy-message) or
            [`message-with-metadata-box`](ref:payload.message-with-metadata-box)
          - `0x81`: outgoing [`message-ack`](ref:payload.message-ack)
          - `0x02`: incoming [`legacy-message`](ref:payload.legacy-message) or
            [`message-with-metadata-box`](ref:payload.message-with-metadata-box)
          - `0x82`: incoming [`message-ack`](ref:payload.message-ack)
          - `0x03`: [`unblock-incoming-messages`](ref:payload.unblock-incoming-messages)
          - `0x20`: [`set-push-notification-token`](ref:payload.set-push-notification-token)
          - `0x21`: (obsolete, formerly used by iOS to set a push filter)
          - `0x22`: (obsolete, formerly used by iOS to set a push sound for contacts)
          - `0x23`: (obsolete, formerly used by iOS to set a push sound for groups)
          - `0x24`: high-priority token for notifications that require
            immediate delivery (e.g. for calls) using the same struct as
            [`set-push-notification-token`](ref:payload.set-push-notification-token)
          - `0x25`: [`delete-push-notification-token`](ref:payload.delete-push-notification-token)
          - `0x30`: [`set-connection-idle-timeout`](ref:payload.set-connection-idle-timeout)
          - `0x31`: (obsolete, formerly used to ensure that a push message is
             sent for all messages, regardless of the flag)
          - `0xd0`: [`queue-send-complete`](ref:payload.queue-send-complete)
          - `0xd1`: (obsolete, formerly used for a function similar to the
             device cookie)
          - `0xd2`: [`device-cookie-change-indication`](ref:payload.device-cookie-change-indication)
          - `0xd3`: [`clear-device-cookie-change-indication`](ref:payload.clear-device-cookie-change-indication)
          - `0xe0`: [`close-error`](ref:payload.close-error)
          - `0xe1`: [`alert`](ref:payload.alert)
        name: type
        type: u8
      - _doc: |-
          Reserved, currently all zeroes.
        name: reserved
        type: b3
      - _doc: |-
          Inner payload. Needs to be parsed according to the `type` field.
        name: data
        type: b*

  echo-request:
    _group: Payloads
    _doc: |-
      An echo request to be answered by a corresponding echo response.

      Can be used for connection keep-alive or RTT estimation.

      Direction: Client <-> Server

      [//]: # "TODO(SE-128)"
    fields:
      - _doc: |-
          Data to be echoed back in the echo response.
        name: data
        type: b*

  echo-response:
    _group: Payloads
    _doc: |-
      An echo response corresponding to an echo request.

      Direction: Client <-> Server

      [//]: # "TODO(SE-128)"
    fields:
      - _doc: |-
          Data echoed back from the echo request.
        name: data
        type: b*

  legacy-message:
    _group: Payloads
    _doc: |-
      An end-to-end encrypted Threema message.

      Direction: Client <-> Server

      Note: This payload is deprecated and may be phased out eventually. It
            will only be used in case the
            [`message-payload-version`](ref:handshake.message-payload-version)
            was not present during login or was explicitly set to the version
            `0x00`.

      Conversion to [`message-with-metadata-box`](ref:payload.message-with-metadata-box):

      - Copy `legacy-message.sender-nickname` to
        `message-with-metadata-box.legacy-sender-nickname`
      - Copy all other fields of `legacy-message` to their respective
        counterparts in `message-with-metadata-box`
      - Set `message-with-metadata-box.metadata-length` to `0`
      - Omit `message-with-metadata-box.metadata-container` (i.e. set it to
        contain 0 bytes)
      - Copy `legacy-message.message-nonce` to
        `message-with-metadata-box.message-and-metadata-nonce`.

      When sending or receiving this payload, convert it to a
      `message-with-metadata-box` and handle it as defined by that struct.

      [//]: # "TODO(SE-128)"
    fields:
      - &message-sender-identity
        _doc: |-
          The sender's Threema ID.
        name: sender-identity
        type: *identity
      - &message-receiver-identity
        _doc: |-
          The receiver's Threema ID.
        name: receiver-identity
        type: *identity
      - &message-message-id
        _doc: |-
          Unique message ID for each sender/receiver pair.

          Used for duplicate detection and for quotes.

          Messages sent in a group must have the same message ID for each group
          member.
        name: message-id
        type: *message-id
      - &message-created-at
        _doc: |-
          Unix timestamp in seconds for when the message has been created.

          Messages sent in a group must have the same timestamp for each group
          member.

          However, the server overrides this timestamp with the current time if

          - the declared timestamp is in the future, or
          - the _short-lived server queuing_ flag was set (`0x20`).

          Note: The original timestamp is still available in an attached
                `csp-e2e.MessageMetadata`.
        name: created-at
        type: u32-le
      - &message-flags
        _doc: |-
          Flags:

          - `0x01`: Send push notification. The server will send a push message
            to the receiver of the message. Only use this for messages that
            require a notification. For example, do not set this for delivery
            receipts.
          - `0x02`: No server queuing. Use this for messages that can be
            discarded by the chat server in case the receiver is not connected
            to the chat server, e.g. the _typing_ indicator.
          - `0x04`: No server acknowledgement. Use this for messages where reliable
            delivery and acknowledgement is not essential, e.g. the _typing_
            indicator. Will not be acknowledged by the chat server when sending.
            No acknowledgement should be sent by the receiver to the chat
            server.
          - `0x10`: Reserved (formerly _group message marker_).
          - `0x20`: Short-lived server queuing. Messages with this flag will
            only be queued for 60 seconds.
          - `0x80`: No automatic delivery receipts. A receiver of a message with this
            flag must not send automatic delivery receipt of type _received_
            (`0x01`) or _read_ (`0x02`). This is not used by the apps but can be
            used by Threema Gateway IDs which do not necessarily want a delivery
            receipt for a message.
        name: flags
        type: u8
      - &message-reserved
        _doc: |-
          Reserved, must be set to zero.
        name: reserved
        type: u8
      - _doc: |-
          Reserved for header compatibility with metadata message.
          Must be set to zero by legacy clients.
        name: reserved-metadata-length
        type: b2
      - _doc: |-
          The sender's public nickname, padded with zeroes if needed.
        name: sender-nickname
        type: b32
      - _doc: |-
          Nonce used for the message box.
        name: message-nonce
        type: *nonce
      - &message-message-box
        _doc: |-
          The message, end-to-end encrypted by:

              XSalsa20-Poly1305(
                key=X25519HSalsa20(<sender.CK>.secret, <receiver.CK>.public),
                nonce=<message-nonce>,
              )
        name: message-box
        type: b*

  message-with-metadata-box:
    _group: Payloads
    _doc: |-
      An end-to-end encrypted Threema message with additional end-to-end
      encrypted metadata.

      Direction: Client <-> Server

      Note: This payload will only be used in case the
            [`message-payload-version`](ref:handshake.message-payload-version)
            was set to version `0x01`.

      Conversion to [`legacy-message`](ref:payload.legacy-message):

      - Discard `message-with-metadata-box.metadata-length` and
        `message-with-metadata-box.metadata-container`
      - Copy `message-with-metadata-box.legacy-sender-nickname` to
        `legacy-message.sender-nickname`
      - Copy `message-with-metadata-box.message-and-metadata-nonce` to
        `legacy-message.message-nonce`.
      - Copy all other fields of `message-with-metadata-box` to their
        respective counterparts in `legacy-message`

      Creating this payload is only allowed as part of the _Bundled Messages
      Send Steps_.

      When receiving this payload:

      1.  (MD) If the device is currently not declared _leader_, exceptionally
          abort these steps and the connection.
      2.  If the nonce of `message-and-metadata-nonce` has been used before, log
          a warning, _Acknowledge_ and discard the message and abort these steps.
      3.  If `receiver-identity` does not equal the user's Threema ID, log a
          warning, _Acknowledge_ and discard the message and abort these steps.
      4.  Run the _Valid Contacts Lookup Steps_ for `sender-identity` and let
          `contact-or-init` be the result.
      5.  If `contact-or-init` indicates that the _contact is the user_ or that
          the _contact is invalid_, log a warning, _Acknowledge_ and discard the
          message and abort these steps.
      6.  If `metadata-length` is greater zero, decrypt the `metadata-container`
          and let `outer-metadata` be the result. If this fails, log a warning,
          _Acknowledge_ and discard the message and abort these steps.
      7.  Decrypt the `message-box`, decode it to a
          [`container`](ref:payload.container) struct and let `outer` be the
          result. If this fails, log a warning, _Acknowledge_ and discard the
          message and abort these steps.
      8.  If `outer.type` is `0xff`, log a warning, _Acknowledge_ and
          discard the message and abort these steps. (Legacy logic, may be
          removed in the future.)
      9.  If `outer.type` is unknown, log a notice, _Acknowledge_ and
          discard the message and abort these steps.
      10. Decode `outer.padded-data` into the message type associated to
          `outer.type` and let `outer-message` be the result. If this fails, log
          a warning, _Acknowledge_ and discard the message and abort these
          steps.
      11. If `outer.type` is not `0xa0`, let `inner-metadata` be
          `outer-metadata`, let `inner-type` be `outer.type` and let
          `inner-message` be `outer-message`.
      12. If `outer.type` is `0xa0`:
          1. Run the receive steps associated to
             `csp-e2e-fs.Envelope` with the decoded `outer-message` and let
             `inner-metadata`, `inner-type`, `inner-message` and `fs-commit-fn`
             be the result. If this fails, exceptionally abort these steps and
             the connection. If the message has been discarded, _Acknowledge_
             and abort these steps.
          2. If `inner-metadata` is not defined, set `inner-metadata` to
             `outer-metadata`.
      13. If `message-id` does not equal `inner-metadata.message_id`, log a
          warning, _Acknowledge_ and discard the message and abort these steps.
      14. If `message-id` refers to a message that has been received previously
          from `sender-identity` (including group messages), log a warning,
          _Acknowledge_ and discard the message and abort these steps.
      15. If `inner-type` is not defined (i.e. handling an FS control message),
          log a notice, _Acknowledge_ and discard the message and abort these
          steps.
      16. If `inner-type` is unknown, log a notice, _Acknowledge_ and
          discard the message and abort these steps.
      17. If `inner-type` is `0xa0` (i.e. FS encapsulation within FS
          encapsulation), log a warning, _Acknowledge_ and discard the message
          and abort these steps.
      18. If `inner-type` is not exempted from blocking, run the _Identity
          Blocked Steps_ for `sender-identity`. If the result indicates that
          `sender-identity` is blocked, _Acknowledge_ and discard the message
          and abort these steps.
      19. If `sender-identity` equals `*3MAPUSH`:
          1. If `inner-type` is not `0xfe`, log a warning,
             _Acknowledge_ and discard the message and abort these steps.
          2. Run the receive steps associated to `inner-type` with
             `inner-message`. If this fails, exceptionally abort these steps and
             the connection. If the message has been discarded, _Acknowledge_
             the message and abort these steps.
      20. If `sender-identity` is not a _Special Contact_:
          1. If `inner-metadata.nickname` is defined, let `nickname` be the
             value of `inner-metadata.nickname`.¹
          2. If `inner-metadata` is not defined and _User Profile Distribution_
             was expected for `inner-type`, let `nickname` be the result of
             decoding the plaintext `legacy-sender-nickname`.¹
          3. If `nickname` is present, trim any excess whitespaces from the
             beginning and the end of `nickname`.
          4. If `contact-or-init` does not contain an existing contact:
             1. If `inner-type` does not require to create an implicit
                _direct_ contact, log a notice, _Acknowledge_ and discard the
                message and abort these steps.
             2. (MD) Run the following sub-steps (labelled _add-contact_):
                1. Begin a transaction with scope `CONTACT_SYNC` and the
                   following precondition:
                   1. If the contact for `sender-identity` exists, abort the
                      _add-contact_ sub-steps.
                2. Reflect a `ContactSync.Create` with `contact` set from
                   `contact-or-init` and the following additional properties:
                   - `created_at` set to now,
                   - `nickname` set to `nickname`,
                   - `acquaintance_level` set to `DIRECT`,
                   - all policies and categories set to their defaults.
                3. Commit the transaction and await acknowledgement.
             3. If the contact for `sender-identity` does not exist, persist a
                new contact from `contact-or-init` and `nickname`.
             4. TODO(SE-510): Schedule fetching gateway-defined profile picture
                here, if contact was added and if necessary.
          5. Lookup the contact associated to `sender-identity` and let
             `contact` be the result (at this point, `contact` must exist).
          6. (MD) If the contact's nickname is different to `nickname`:
            1. Begin a transaction with scope `CONTACT_SYNC` and the
                following precondition:
                1. If the contact no longer exists, log an error and
                  exceptionally abort these steps and the connection.
            2. Reflect a `ContactSync.Update` with `contact` including the
               new `nickname`.
            3. Commit the transaction and await acknowledgement.
          7. Update the contact's nickname with `nickname`. Remove the
             contact's nickname if `nickname` is empty.
          8. Run the receive steps associated to `inner-type` with
             `inner-message`. If this fails, exceptionally abort these steps and
             the connection. If the message has been discarded, _Acknowledge_
             the message and abort these steps.
      21. (MD) If the properties associated to `inner-type` require
          reflecting incoming messages, reflect a `d2d.IncomingMessage` from
          `outer-type` and `outer-message` and the associated conversation to
          other devices and wait for reflection acknowledgement.² If this fails,
          exceptionally abort these steps and the connection.³
      22. _Acknowledge_ the message.⁴
      23. If the properties associated to `type` do not require sending
          automatic delivery receipts or `flags` contains the _no
          automatic delivery receipts_ (`0x80`) flag, abort these steps.
      24. Run the _Bundled Messages Send Steps_ with the following properties:
          - `id` being a random message ID,
          - `created-at` set to the current timestamp,
          - `receivers` set to `contact`,
          - to construct a [`delivery-receipt`](ref:e2e.delivery-receipt)
            message with status _received_ (`0x01`) and the respective
            `message-id`.

      ¹: Note that the `nickname` of `MessageMetadata` may be undefined (leading
      to no changes) or defined but explicitly empty (leading to the nickname of
      the contact being removed) which is an important semantic difference.
      Unlike the legacy nickname field which always contains a value and
      therefore cannot represent this semantic difference without having to
      check whether _User Profile Distribution_ was required for the type.

      ²: We reflect the **outer** message container depending on the unwrapped
      **inner** message type, so the forward security properties are untouched
      and all other devices need to go through the same process.

      ³: Reflection needs to happen after the message has been processed and all
      side effects have been applied. Otherwise, if the receive process is
      interrupted and another device takes over, it would discard the message as
      a duplicate.

      ⁴: Because the message is already acknowledged towards the server here,
      the following steps may not get executed at all if the connection drops
      or the execution fails. All following steps must therefore be
      non-critical.

      The following steps are defined as _Acknowledge_ steps for an incoming
      message:

      1. If the steps for this message have already been invoked once, abort
         these steps.
      2. If `flags` does not contain the _no server acknowledgement_ (`0x04`)
         flag, send a [`message-ack`](ref:payload.message-ack) payload to the
         chat server with the respective `message-id`.
      3. If the properties associated to `inner-type` require protection against
         replay, mark the nonce of `message-and-metadata-nonce` as used.
      4. If `fs-commit-fn` is defined, run it.

      [//]: # "TODO(SE-128)"
    fields:
      - *message-sender-identity
      - *message-receiver-identity
      - *message-message-id
      - *message-created-at
      - *message-flags
      - *message-reserved
      - _doc: |-
          Length of the metadata box. In case it is zero, no metadata is
          present (for compatibility with clients using
          [`legacy-message`](ref:payload.legacy-message)).

          Note: For outgoing messages, a metadata box should always be present.
        name: metadata-length
        type: u16-le
      - _doc: |-
          Backwards compatibility field for the sender's public nickname.
          Padded with zeroes if needed.

          When sending a message towards a Threema Gateway ID (starts with a
          `*`), add the same nickname as included in the encrypted metadata box.
          Otherwise, set it to all zeroes.

          Note: The backwards compatibility for Threema Gateway IDs will be
          removed eventually!
        name: legacy-sender-nickname
        type: b32
      - _doc: |-
          Metadata associated to the message. Must be ignored in case
          `metadata-length` is zero.

          Message Metadata Key (`MMK`) derivation:

              S = X25519HSalsa20(<sender.CK>.secret, <receiver.CK>.public)
              MMK = BLAKE2b(key=S, salt='mm', personal='3ma-csp')

          The encoded `csp-e2e.MessageMetadata` is then encrypted in the
          following way:

              XSalsa20-Poly1305(
                key=MMK,
                nonce=<message-with-metadata-box.message-and-metadata-nonce>,
              )
        name: metadata-container
        type: b{metadata-length}
      - _doc: |-
          Nonce used for the message and the metadata box.
        name: message-and-metadata-nonce
        type: *nonce
      - *message-message-box

  message-ack:
    _group: Payloads
    _doc: |-
      Acknowledges that a message has been received.

      Direction: Client <-> Server

      [//]: # "TODO(SE-128)"
    fields:
      - _doc: |-
          Identity of the sender for an incoming (`0x82`) message / of the
          receiver for an outgoing (`0x81`) message.
        name: identity
        type: *identity
      - _doc: |-
          Refers to the `message-id` of the acknowledged message.
        name: message-id
        type: *message-id

  unblock-incoming-messages:
    _group: Payloads
    _doc: |-
      Unblock incoming messages from the server. Sent by a multi-device capable
      client once it is nominated to receive incoming messages.

      Direction: Client --> Server

      [//]: # "TODO(SE-128)"

  set-push-notification-token:
    _group: Payloads
    _doc: |-
      Sets the push notification token to be used when sending a push message.

      Direction: Client --> Server
    fields:
      - _doc: |-
          Type of the push token:

          - `0x00`: No push
          - `0x01`: APNs Production
          - `0x02`: APNs Development
          - `0x03`: APNs Production with `content-available` key
          - `0x04`: APNs Development with `content-available` key
          - `0x05`: APNs Production with `mutable-content` key
          - `0x06`: APNs Development with `mutable-content` key
          - `0x11`: FCM with empty payload
          - `0x13`: HMS with empty payload
        name: type
        type: u8
      - _doc: |-
          Push token, maximum 255 bytes.
        name: token
        type: b*

  delete-push-notification-token:
    _group: Payloads
    _doc: |-
      Deletes push tokens for a Threema ID. Can be used when self-removing or
      removing another device from a device group.

      Direction: Client --> Server

      When receiving this payload:

      1. If `csp-device-ids` is empty, delete all tokens for all devices except
         the device sending the payload and abort these steps.
      2. Delete all tokens for the devices specified in `csp-device-ids`.
    fields:
      - _doc: |-
          Delete tokens belonging to a
          [`csp-device-id`](ref:handshake.csp-device-id) in the same device
          group.
        name: csp-device-ids
        type: u64-le[]

  set-connection-idle-timeout:
    _group: Payloads
    _doc: |-
      Request a different idle timeout than the default one of 5 minutes. The
      new setting is valid for the connection only.

      The client must ensure that it sends echo requests or other traffic
      frequently to keep the connection alive.

      Direction: Client --> Server

      [//]: # "TODO(SE-128)"
    fields:
      - _doc: |-
          Idle timeout in seconds. Minium 30s, maximum 600s.
        name: timeout
        type: u16-le

  queue-send-complete:
    _group: Payloads
    _doc: |-
      Indicates that the incoming message queue on the server has been fully
      transmitted to the client. A client should not disconnect prior to
      having received this payload.

      Direction: Client <-- Server

      [//]: # "TODO(SE-128)"

  device-cookie-change-indication:
    _group: Payloads
    _doc: |-
      Indicates to the client that a device cookie mismatch has been detected
      since the last time that the device cookie change indication has been
      cleared (using the
      [`clear-device-cookie-change-indication`](ref:clear-device-cookie-change-indication)
      payload).

      The client should display a warning in form of a notification and/or
      dialog to the user, informing them that a new and potentially unauthorized
      device has accessed the account. When the user confirms, the client should
      send a
      [`clear-device-cookie-change-indication`](ref:clear-device-cookie-change-indication)
      payload to clear the indication.

      Direction: Client <-- Server

  clear-device-cookie-change-indication:
    _group: Payloads
    _doc: |-
      Causes the server to clear the flag that triggers sending the
      [`device-cookie-change-indication`](ref:device-cookie-change-indication)
      on each connection.

      The flag will be set again by the server if another device cookie
      mismatch is detected.

      Direction: Client --> Server

  close-error:
    _group: Payloads
    _doc: |-
      Indicates that the connection has experienced an unrecoverable error and
      must be closed.

      Direction: Client <-- Server

      [//]: # "TODO(SE-128)"
    fields:
      - _doc: |-
          Indicates whether the client is allowed to reconnect automatically
          after the connection has been severed. This allows the server to
          prevent infinite loops in case of a recurring error.

          Set to `0` in case the client may not reconnect automatically or any
          other value otherwise.
        name: can-reconnect
        type: u8
      - _doc: |-
          Error message (UTF-8 encoded)
        name: message
        type: b*

  alert:
    _group: Payloads
    _doc: |-
      Generic alert that should be displayed in the client's user interface.

      Direction: Client <-- Server

      [//]: # "TODO(SE-128)"
    fields:
      - _doc: |-
          Alert message (UTF-8 encoded)
        name: message
        type: b*

# End-to-end encrypted structs
e2e: &e2e
  _doc: |-
    ## End-to-End Encrypted Messages

    An end-to-end encrypted message can be sent or received once the handshake
    was successful. Every end-to-end encrypted message is wrapped inside of a
    [`container`](ref:payload.container) struct that is then encrypted and
    wrapped by a payload [`legacy-message`](ref:payload.legacy-message) or
    [`message-with-metadata-box`](ref:payload.message-with-metadata-box)
    struct.

    ### Predefined Contacts

    A pedefined contacts can be added to the contact list and is automatically
    initialised with its identity, nickname, hard-coded public key and the verification
    level _fully verified_. Once a predefined contact is in the contact list, it
    is treated like any other normal contact (with editable properties like
    first and last name, etc.).

    A predefined contact may be marked _special_ meaning it follows special
    logic. These are also known as _Special Contact_s. Even though special
    contacts should not normally appear in the contact list, there's nothing
    stopping a user from adding a special contact to its contact list. While
    they are treated like normal contacts in the contact list, depending on the
    special handling logic, it may not be possible to send or receive normal
    messages from them.

    The following list contains all predefined contacts:

    - `*3MAPUSH`:
      - Nickname: Threema Push
      - Public Key:
        - Production: fd711e1a0db0e2f03fcaab6c43da2575b9513664a62a12bd0728d87f7125cc24
        - Sandbox: fd711e1a0db0e2f03fcaab6c43da2575b9513664a62a12bd0728d87f7125cc24
      - Special: Yes

    - `*3MATOKN`:
      - Nickname: Threema Token
      - Public Key:
        - Production: 04884d12d668f855d00d71fb1d9d413c95f271312f7e077846af671875c4101b
      - Special: No

    - `*3MAWORK`:
      - Nickname: Threema Work Channel
      - Public Key:
        - Production: 9aa0a72a8fb6f0cc53727fea6096f1b7b0ebefcc2650ad39a1e54837bba0bc4b
        - Sandbox: 9aa0a72a8fb6f0cc53727fea6096f1b7b0ebefcc2650ad39a1e54837bba0bc4b
      - Special: No

    - `*BETAFBK`:
      - Nickname: Threema Beta Feedback
      - Public Key:
        - Production: 5684d6dcd32a16488df8371095fc9a1fc25baeb6b97366d99fdf2aba00e2bc5c
      - Special: No

    - `*MY3DATA`:
      - Nickname: My Threema Data
      - Public Key:
        - Production: 3b01854f24736e2d0d2dc387eaf2c0273c5049052147132369bf3960d0a0bf02
        - Sandbox: 83adfee6558b68ae3cd6bbe2a33f4e4409d5624a7cea23a18975aea6272a0070
      - Special: No

    - `*SUPPORT`:
      - Nickname: Threema Support
      - Public Key:
        - Production: 0f944d18324b2132c61d8e40afce60a0ebd701bb11e89be94972d4229e94722a
        - Sandbox: 0f944d18324b2132c61d8e40afce60a0ebd701bb11e89be94972d4229e94722a
      - Special: No

    - `*THREEMA`:
      - Nickname: Threema Channel
      - Public Key:
        - Production: 3a38650c681435bd1fb8498e213a2919b09388f5803aa44640e0f706326a865c
        - Sandbox: 3a38650c681435bd1fb8498e213a2919b09388f5803aa44640e0f706326a865c
      - Special: No

    Note: OnPrem provisions predefined contacts in the associated OPPF file.

    ### Mitigating Replay

    To prevent replay attacks, a client must permanently store used nonces for
    incoming and outgoing end-to-end encrypted messages. Messages reusing
    previously used nonces must not be processed and discarded. One nonce
    store for all end-to-end encrypted messages across different contacts is
    sufficient.

    Note that it is still possible for the chat server to replay old messages to
    a device whose database has been erased (e.g. when restoring a backup).
    However, this is not applicable to forward security encrypted messages.

    ### Message ID

    Each message has an associated message ID. It is crucial to understand
    that this is not a unique identifier across multiple conversations.
    Unique identification of a message is determined by:

    - 1:1 Chats: The message ID in combination with the contact's Threema ID.
    - Group Chats: The message ID in combination with the group creator's
      Threema ID and the group ID.
    - Distribution Lists: The message ID with an artificial distribution list
      ID.

    When a message is being quoted, it may only be looked up within the
    associated conversation.

    ### Flags

    For each message, we will define _mandatory_ and _optional_ flags
    referring to the `flags` field of the payload
    [`legacy-message`](ref:payload.legacy-message) or
    [`message-with-metadata-box`](ref:payload.message-with-metadata-box)
    struct. A flag must be considered _mandatory_ unless it has been explicitly
    marked _optional_.

    ### Delivery Receipts

    There are two types of delivery receipts (sent using the
    [`delivery-receipt`](ref:e2e.delivery-receipt) message):

    - Automatic: "received" and "read"
    - Manual: "acknowledged" and "declined"

    For each message, we will define whether automatic delivery receipts should
    be sent and whether it is eligible for sending manual delivery receipts
    (e.g. acknowledge/decline). However, two general exceptions apply:

    1. Automatic delivery receipts are not sent to group members (i.e. when
       any message struct is wrapped in a `group` message struct).
    2. Messages whose flags include `0x80` must not trigger any automatic
       delivery receipts.

    ### Blocking

    The sender Threema ID may be blocked explicitly (i.e. blocking a specific
    Threema ID) or implicitly (blocking all unknown Threema IDs). This does not
    require special handling on the server but instead is done entirely by the
    clients.

    Note that the protocol does not distinguish between implicitly and
    explicitly blocked Threema IDs. An implicitly blocked Threema ID (i.e.
    blocking unknown contacts) must be treated the same as an explicitly blocked
    Threema ID (i.e. blocking specific contacts).

    The UI must prevent users from composing or submitting messages towards a
    blocked contact. In practise, this is only relevant for explicitly blocked
    contacts.

    The following steps are defined as the _Identity Blocked Steps_:

    1.  Let `identity` be the Threema ID to be checked.
    2.  If `identity` is a _Special Contact_, return that it is not blocked.
    3.  If `identity` is explicitly blocked, return that it is blocked.
    4.  If the settings indicate that unknown contacts should not be blocked,
        return that it is not blocked.
    5.  If `identity` is a _Predefined Contact_, return that it is not blocked.
    6.  Let `contact` be the associated contact to `identity`.
    7.  If `contact` is not defined, return that it is blocked.
    8.  If `contact` has acquaintance level _direct_, return that it is not
        blocked.
    9.  If `contact` is part of a group that is not marked as _left_, return
        that it is not blocked.
    10. Return that it is blocked.

    ### Contact Flows

    The following steps are defined as the _Valid Contacts Lookup Steps_:

    1.  Let `identities` be the identities to look up.
    2.  Let `contact-or-inits` be an empty map of Threema IDs to a contact,
        properties to create a contact from, or _contact is the user_ or
        _contact is invalid_ marker.
    3.  Let `unknown-identities` be an empty list.
    4.  For each `identity` of `identities`:
        1. If `identity` equals the user's Threema ID, add the information
           that the _contact is the user_ to the `contact-or-inits` map and
           abort these sub-steps.
        2. If `identity` is a _Special Contact_, add that special contact to the
           `contact-or-inits` map and abort these sub-steps.
        3. Lookup the contact associated to `identity` and let `contact` be the
           result.
        4. If `contact` is defined, add `contact` to the `contact-or-inits` map
           and abort these sub-steps.
        5. Lookup the properties to create a contact from associated to
           `identity` from the _contact lookup cache_ and let `init` be the
           result.
        6. If `init` is defined, add `init` to the `contact-or-inits` map and
           abort these sub-steps.
        7. Add `identity` to `unknown-identities`.
    5.  Let `directory-response` be the response of asynchronously looking up
        `unknown-identities` on the Directory Server.
    6.  If work build, let `work-directory-response` be the response of
        asynchronously looking up `unknown-identities` on the Work Contacts API
        endpoint.
    7.  Await `directory-response` and `work-directory-response`.
    8.  Process the result of `directory-response`:
        1. If the server could not be reached, exceptionally abort these steps.
        2. If the status code is `429`, exceptionally abort these steps and add
           a minimum delay of 10s before retrying a connection.
        3. If the status code is not `200`, exceptionally abort these steps.
        4. For each contact entry of the result:
           1. Remove the contact from `unknown-identities`. If it was not
              present in `unknown-identities`, log a warning and abort these
              sub-steps.
           2. If the contact is marked as _invalid_ (never existed or has been
              revoked), add the information that the _contact is invalid_ to the
              `contact-or-inits` map and abort these sub-steps.
           3. If the contact is a _Predefined Contact_:
              1. If the contact's public key does not equal the _Predefined
                 Contact_s public key, log a warning and exceptionally abort these
                 steps.
              2. Update the contact information with the following information:
                 - set `verification_level` to `FULLY_VERIFIED`,
                 - set `nickname` to the nickname of the _Predefined Contact_,
                 - if _Predefined Contact_ defines a first name, set it
                   accordingly,
                 - if _Predefined Contact_ defines a last name, set it
                   accordingly,
           3. Add the resulting contact information to the `contact-or-inits` map
              from which a new contact can be created.
        5. For each `identity` of `unknown-identities`:
           1. Add the information that the _contact is invalid_ to the
              `contact-or-inits` map for `identity`.
        6. Clear `unknown-identities`.
    9.  If `work-directory-response` is defined, process its result:
        1. If the server could not be reached, exceptionally abort these steps.
        2. If the status code is `401`, exceptionally abort these steps,
           notify the user that the Work credentials are invalid and request new
           ones. The connection should not be retried until new Work credentials
           have been entered and checked for validity.
        3. If the status code is `429`, exceptionally abort these steps and add a
           minimum delay of 10s before retrying a connection.
        4. If the status code is not `200`, exceptionally abort these steps.
        5. For each `work-contact` of the result:
           1. If an entry for the `work-contact`'s identity does not exist in
              `contact-or-inits`, log a warning and abort these sub-steps.
           2. If `work-contact`'s public key does not equal `contact-or-init`'s
              public key, log a warning and exceptionally abort these steps.
           3. Update the contact entry for `work-contact`'s identity in
              `contact-or-inits` with the following information:
              - if `verification_level` is not defined or `UNVERIFIED`, set it
                to `SERVER_VERIFIED`,
              - set `work_verification_level` to `WORK_SUBSCRIPTION_VERIFIED`,
              - if `work-contact.first-name` is defined, set the first name
                accordingly,
              - if `work-contact.last-name` is defined, set the last name
                accordingly,
    10. TODO(SE-173): Run the contact import flow for `contact-or-inits` and
        update the `verification_level` for all whose associated phone number /
        email could be matched. Import `first_name` and `last_name` (if not
        already defined) and set `sync_state` to `IMPORTED`. Clarify precedence
        regarding Work API.
    11. For each `init` of `contact-or-inits`:
        1. If `init` does not contain properties to create a contact from (i.e.
           it is a contact or any of the special markers), abort these
           sub-steps.
        2. If `init.sync_state` is not defined, set it to `INITIAL`.
        3. If `init.verification_level` is not defined, set it to `UNVERIFIED`.
        4. If `init.work_verification_level` is not defined, set it to `NONE`.
    12.  Update the _contact lookup cache_ with the contents of
        `contact-or-inits`. Each newly added or updated entry has an expiration
        time of 10m after which the entry is to be removed from the cache.
    13. Return `contact-or-inits`.

    ### Groups

    Groups are handled in a decentralised manner. Messages are sent to each
    group member individually. On a technical level, a group is identified by
    **both** the Threema ID of the creator and the random group ID the creator
    chose. A group **must never** be identified by the group ID alone.

    Group messages are special containers wrapped around normal messages (it is
    actually just a common header):

    - [`group-member-container`](ref:e2e.group-member-container): For group
      message communication between members, including the creator.
    - [`group-creator-container`](ref:e2e.group-creator-container): For special
      messages that may only be sent from the creator to normal group members
      and vice versa.

    Group messages have special types in order to separate them from other
    messages. These types also define which container must be used.

    The group members are determined by the
    [`group-setup`](ref:e2e.group-setup) message and continuously updated by
    any following [`group-leave`](ref:e2e.group-leave) messages. Any following
    [`group-setup`](ref:e2e.group-setup) overrides the previous member state.

    ### Implicit Contact Creation

    When the user is added to a group, every unknown member of the group must be
    added to the contact list with acquaintance level _group_. Messages from a
    contact with any acquaintance level will not be implicitly blocked by a
    _block unknown_ setting.

    The contact remains at the acquaintance level _group_ until a 1:1
    conversation with that contact is being started by either side in which
    case the acquaintance level should be changed to _direct_.

    A contact with acquaintance level _group_ will remain indefinitely even if
    the contact is being removed from all groups of the user or if all remaining
    common groups are marked as _left_. In that case, the contact is implicitly
    marked as _deleted_ so that it is covered by _block unknown_.

    ### Notes Group

    A group is identified as a _notes_ group if all of the following criteria
    are met:

    1. The user is the creator of the group.
    2. The group currently has no members (besides the creator).
    3. The group is not marked as _left_.

    Messages in a _notes_ group are synchronised across devices but are not
    sent to the chat server (since there are no other members). Therefore,
    it is ideal for "notes to self", hence the name.

    A group seamlessly transforms into a _notes_ group and out of it given the
    above criteria. Right now this can happen in three scenarios:

    - A _notes_ group is created explicitly (i.e. a group with only the user
      is being created).
    - The user is the creator of a group and one or more members are being
      added in which case the _notes_ group transforms into a regular group.
    - The user is the creator of a group whose members have just been removed
      (but the group has not been disbanded) in which case the group
      transforms into a _notes_ group.

    The UI should signal the _notes_ status of a group to the user.

    ### Group Flows

    The following steps are defined as the _Active Group Update Steps_:

    1.  If the user is not the creator of the group or the group is marked as
        _left_, log an error and abort these steps.
    2.  Let `message-ids` be a list of four pre-generated message IDs.
    3.  Let `changes` be the set of expected changes to the group which may
        contain the following properties:
        - `profile-picture` is defined if the group's profile picture is
          expected to be changed and contains either the new profile picture or
          a _remove_ mark to remove it
        - `profile-picture.blob` may contain the associated blob information
          data in case of a changed profile picture.
        - `add-members` is a set of new members to be added to the group
        - `remove-members` is a set of existing members to be removed from the
          group
    4.  Let `group` be a snapshot of the current group state.
    5.  Remove all members from `changes.add-members` that are not in
        `group.members`.
    6.  Remove all members from `changes.remove-members` that are in
        `group.members`.
    7.  Let `messages` be an empty list.
    8.  If `changes.remove-members` is not empty, add a message entry to
        `messages` to remove members to be removed with the following
        properties:
        - `id` set to the first message ID of `message-ids`,
        - `created-at` set to the current timestamp,
        - `receivers` set to `changes.remove-members`,
        - to construct a [`group-setup`](ref:e2e.group-setup) (wrapped by
          [`group-creator-container`](ref:e2e.group-creator-container))
          with an empty members set.
    9.  If `group.members` is not empty:
        1. Add a message entry to `messages` to update the group for the
            members with the following properties:
            - `id` set to the first message ID of `message-ids`,
            - `created-at` set to the current timestamp,
            - `receivers` set to `group.members`,
            - to construct a [`group-setup`](ref:e2e.group-setup) (wrapped by
              [`group-creator-container`](ref:e2e.group-creator-container))
              from `group.members`.
        2. Add a message entry to `messages` to announce the group's name to
            the members with the following properties:
            - `id` set to the second message ID of `message-ids`,
            - `created-at` set to the current timestamp,
            - `receivers` set to `group.members`,
            - to construct a [`group-name`](ref:e2e.group-name) (wrapped by
              [`group-creator-container`](ref:e2e.group-creator-container))
              from `group.name`.¹
        3. If `group.profile-picture` is defined:
            1. Let `profile-picture-blob` be `changes.profile-picture.blob`.
            2. If `group.profile-picture` does not equal
               `changes.profile-picture`, upload `group.profile-picture` to the
               blob server with the _persist_ flag and set
               `profile-picture-blob` to the result.
        4. Add a message entry to `messages` to announce the group's profile
            picture to the members with the following properties:
            - `id` set to the third message ID of `message-ids`,
            - `created-at` set to the current timestamp,
            - `receivers` set to `group.members`,
            - to construct a
              [`set-profile-picture`](ref:e2e.set-profile-picture) (wrapped by
              [`group-creator-container`](ref:e2e.group-creator-container)) from
              `profile-picture-blob` if `profile-picture-blob` is defined or
              [`delete-profile-picture`](ref:e2e.delete-profile-picture)
              (wrapped by
              [`group-creator-container`](ref:e2e.group-creator-container))
              otherwise.¹
        5. Let `chosen-call` be the result of the most recent invocation of
            the _Group Call Refresh Steps_ for the group.
        6. If `chosen-call` is defined, add a message entry to `messages` to
            announce the ongoing group call to newly added members with the
            following properties:
            - `id` set to the fourth message ID of `message-ids`,
            - `created-at` set to the `started_at` value associated to
              `chosen-call`,
            - `receivers` set to `changes.add-members`,
            - to construct a repeat of `csp-e2e.GroupCallStart` (wrapped by
              [`group-member-container`](ref:e2e.group-member-container))
              that is associated to `chosen-call`.
    10. Run the _Bundled Messages Send Steps_ with `messages`.

    ¹: This results in the group name and the group profile picture being
    distributed to all members regardless of whether it was changed or not. This
    is deemed acceptable for the sake of implementation simplicity and
    reusability.

    The following steps are defined as the _Active Group State Resync Steps_:

    1.  If the user is not the creator of the group or the group is marked as
        _left_, log an error and abort these steps.
    2.  Let `message-ids` be a list of four pre-generated message IDs.
    3.  Let `target-members` be a set of members to receive the resync.
    4.  Remove all members from `target-members` that are not in
        `group.members`.
    5.  If `target-members` is empty, abort these steps.
    6.  Let `messages` be an empty list.
    7.  Add a message entry to `messages` to announce the group composition with
        the following properties:
        - `id` set to the first message ID of `message-ids`,
        - `created-at` set to the current timestamp,
        - `receivers` set to `target-members`,
        - to construct a [`group-setup`](ref:e2e.group-setup) (wrapped by
          [`group-creator-container`](ref:e2e.group-creator-container)) from
          `group.members`.
    8.  Add a message entry to `messages` to announce the group's name with the
        following properties:
        - `id` set to the second message ID of `message-ids`,
        - `created-at` set to the current timestamp,
        - `receivers` set to `target-members`,
        - to construct a [`group-name`](ref:e2e.group-name) (wrapped by
          [`group-creator-container`](ref:e2e.group-creator-container)) from
          `group.name`.
    9.  If `group.profile-picture` is defined, upload `group.profile-picture` to
        the blob server with the _persist_ flag and let `profile-picture-blob`
        be the result.
    10. Add a message entry to `messages` to announce the group's profile
        picture with the following properties:
        - `id` set to the third message ID of `message-ids`,
        - `created-at` set to the current timestamp,
        - `receivers` set to `target-members`,
        - to construct a
          [`set-profile-picture`](ref:e2e.set-profile-picture) (wrapped by
          [`group-creator-container`](ref:e2e.group-creator-container)) from
          `profile-picture-blob` if `profile-picture-blob` is defined or
          [`delete-profile-picture`](ref:e2e.delete-profile-picture) (wrapped by
          [`group-creator-container`](ref:e2e.group-creator-container))
          otherwise.
    11. Let `chosen-call` be the result of the most recent invocation of
        the _Group Call Refresh Steps_ for the group.
    12. If `chosen-call` is defined, add a message entry to `messages` to
        announce the ongoing group call with the following properties:
        - `id` set to the fourth message ID of `message-ids`,
        - `created-at` set to the `started_at` value associated to
              `chosen-call`,
        - `receivers` set to `target-members`,
        - to construct a repeat of `csp-e2e.GroupCallStart` (wrapped by
          [`group-member-container`](ref:e2e.group-member-container)) that is
          associated to `chosen-call`.
    13. Run the _Bundled Messages Send Steps_ with `messages`.
    14. For each member of `target-members`, mark the group as _recently
        resynced_ for 1h.

    #### Create/Clone Group

    The following steps must be invoked when the user wants to create or clone a
    group:

    1.  Let `init` contain the following properties to create a group:
        - `name` of the new group or an empty string
        - `profile-picture` of the new group or undefined
        - `members` is a set of initial members to be added to the group¹
    2.  Let `parameters` be the MDM parameters. If
        `parameters.th_disable_create_group` is `true`, abort these steps.
    3.  Let `group-id` be a random group ID.
    4.  (MD) Begin a transaction with scope `GROUP_SYNC` and the following
        precondition:
        1. If a group with `group-id` and the user as creator exists, log an
           error and abort these steps.
        2. If `init.members` contains a member that is not an existing
           contact, log an error and abort these steps.
    5.  If `init.profile-picture` is defined, upload `init.profile-picture` to
        the blob server with the _persist_ flag and set
        `init.profile-picture.blob` to the result.
    6.  (MD) Reflect a `GroupSync.Create` with `group` set to contain:
        - `group_identity` being `group-id` and the user as the creator,
        - `created_at` set to now,
        - `name` set to `init.name`
        - `user_state` set to `MEMBER`,
        - `profile_picture` set from `init.profile-picture.blob`,
        - `member_identities` set from `init.members`,
        - all policies and categories set to their defaults.
    7.  (MD) Commit the transaction and await acknowledgement.
    8.  Persist the newly created group from `init` and `group-id` to storage.
    9.  If `init.members` is empty, abort these steps.
    10. Let `message-ids` be a list of four random message IDs.
    11. Schedule a persistent task to run the following steps:
        1. (MD) Begin a transaction with scope `GROUP_SYNC` and the following
           precondition:
           1. If the group does not exist or the group is marked as _left_ or the
              group has no members, log a warning that a group sync race occurred
              and abort these steps.
        2. Let `group` be a snapshot of the current group state.
        3. If any of the following cases is true, log a warning that a group
           sync race occurred:
           - `init.name` is defined and does not equal `group.name`,
           - `init.profile-picture` does not equal `group.profile-picture`,
           - `init.members` does not equal `group.members`.
        4. Run the _Active Group Update Steps_ with `message-ids` and the
           following expected set of `changes`:
           - `profile-picture` set to `init.profile-picture`,
           - `add-members` set to `init.members`,
           - `remove-members` set to an empty list.
        5. (MD) Commit the transaction and await acknowledgement.

    ¹: Note that all contacts must be added before they can be added as initial
    members of the group.

    #### Update Group

    The following steps must be invoked when the user is the creator of a group
    and intends to apply a change to the group's name, profile picture or
    add/remove members to/from the group:

    1.  If the user is not the creator of the group or the group is marked as
        _left_, log an error and abort these steps.
    2.  Let `changes` be the set of changes to the group which may contain the
        following properties:
        - `name` is defined if the group's name is to be changed and contains the
          new name or an empty string
        - `profile-picture` is defined if the group's profile picture is to be
          changed and contains either the new profile picture or a _remove_ mark
          to remove it
        - `add-members` is a set of new members to be added to the group¹
        - `remove-members` is a set of existing members to be removed from the
          group¹
    3.  (MD) Begin a transaction with scope `GROUP_SYNC` and the following
        precondition:
        1. If `changes.add-members` or `changes.remove-members` contains a
           member that is not an existing contact, log an error and abort these
           steps.
        2. If the group does not exist or the group is marked as _left_, log a
           warning and abort these steps.
    4.  Let `updated-members` be a copy of the current member set of the group.
        Add all `changes.add-members` to this set that are to be added to the
        group. Remove all `changes.remove-members` from this set that are to be
        removed from the group.
    5.  If `changes.profile-picture` is defined and contains a profile picture,
        upload `changes.profile-picture` to the blob server with the _persist_
        flag and let `changes.profile-picture.blob` be the result.
    6.  (MD) Reflect a `GroupSync.Update` with `member_state_changes`
        constructed from `changes.add-members` and `changes.remove-members` and
        `group` set to contain:
        - `name` set to `changes.name`,
        - `profile_picture` set according to `changes.profile-picture` (and
          `changes.profile-picture.blob`),
        - `member_identities` set from `updated-members`.
    7.  (MD) Commit the transaction and await acknowledgement.
    8.  If the user is currently participating in a group call of this group,
        remove all `change.remove-members` participants from the group call
        (handle them as if they left the call).
    9.  Persist the `updated-members` and other `changes` to the group.
    10. If `changes.add-members` or `changes.remove-members` is not empty, run
        the _Rejected Messages Refresh Steps_ for the group.
    11. Let `message-ids` be a list of four random message IDs.
    12. Schedule a persistent task to run the following steps:
        1. (MD) Begin a transaction with scope `GROUP_SYNC` and the following
           precondition:
           1. If the group does not exist or the group is marked as _left_, log
              a warning that a group sync race occurred and abort these steps.
        2. Let `group` be a snapshot of the current group state.
        3. If any of the following cases is true, log a warning that a group
           sync race occurred:
           - `changes.name` is defined and does not equal `group.name`,
           - `changes.profile-picture` contains a profile picture and does not
              equal `group.profile-picture`,
           - `changes.profile-picture` contains the _remove_ mark and
              `group.profile-picture` is defined,
           - `updated-members` does not equal `group.members`.
        4. Run the _Active Group Update Steps_ with `message-ids` and `changes`.
        5. (MD) Commit the transaction and await acknowledgement.

    ¹: Note that all contacts must be added before they can be added as members
    to the group. The same applies to members that are being removed, obviously.

    #### Disband/Remove Group

    The following steps must be invoked when the user is the creator of a group
    and intends to _disband_ or _disband and remove_ the group:

    1.  Let `intent` be the user's intent which can be either to _disband_ or to
        _disband and remove_ the group.
    2.  If the user is not the creator of the group or the group is marked as
        _left_, log an error and abort these steps.
    3.  (MD) Begin a transaction with scope `GROUP_SYNC` and the following
        precondition:
        1. If the group does not exist or the group is marked as _left_, log a
           warning and abort these steps.
    4.  (MD) If `intent` is to _disband_, reflect a `GroupSync.Update` with
        `group` set to contain `user_state` set to `LEFT`.
    5.  (MD) If `intent` is to _disband and remove_, reflect a
        `GroupSync.Delete` for this group.
    6.  (MD) Commit the transaction and await acknowledgement.
    7.  If the user is participating in a group call of this group, trigger
        leaving the call.
    8.  If the `intent` is to _disband_:
        1. Mark the group as _left_.
        2. Persist the previous member setup so that the group can be cloned.
        3. Run the _Rejected Messages Refresh Steps_ for the group.
    9.  If the `intent` is to _disband and remove_, remove the group and all
        associated messages from storage.
    10. Let `message-id` be a random message ID.
    11. Schedule a persistent task to run the following steps:
        1. (MD) Begin a transaction with scope `GROUP_SYNC` and the following
           precondition:
           1. If the group exists and is not marked as _left_, log an error that
              a major group state inconsistency has been detected¹ and abort
              these steps.
        2. Let `group` be a snapshot of the current group state.
        3. Run the _Bundled Messages Send Steps_ with the following properties:
           - `id` set to `message-id`,
           - `created-at` set to the current timestamp,
           - `receivers` set to `group.members`,
           - to construct a [`group-setup`](ref:e2e.group-setup) (wrapped by
             [`group-creator-container`](ref:e2e.group-creator-container)) with
             an empty members set.
        4. (MD) Commit the transaction and await acknowledgement.

    ¹: Disbanding a group as the creator makes the group strictly non-reusable.

    #### Leave/Remove Group

    The following steps must be invoked when the user is not the creator of a
    group and intends to _leave_ or _leave and remove_ the group:

    1.  Let `intent` be the user's intent which can be either to _leave_ or to
        _leave and remove_ the group.
    2.  If the user is the creator of the group or the group is marked as
        _left_, log an error and abort these steps.
    3.  (MD) Begin a transaction with scope `GROUP_SYNC` and the following
        precondition:
        1. If the group does not exist or the group is marked as _left_, log a
           warning and abort these steps.
    4.  (MD) If `intent` is to _leave_, reflect a `GroupSync.Update` with
        `group` set to contain `user_state` set to `LEFT`.
    5.  (MD) If `intent` is to _leave and remove_, reflect a `GroupSync.Delete`
        for this group.
    6.  (MD) Commit the transaction and await acknowledgement.
    7.  If the user is participating in a group call of this group, trigger
        leaving the call.
    8.  If the `intent` is to _leave_:
        1. Mark the group as _left_.
        2. Persist the previous member setup so that the group can be cloned.
        3. Run the _Rejected Messages Refresh Steps_ for the group.
    9.  If the `intent` is to _leave and remove_, remove the group and all
        associated messages from storage.
    10. Let `message-id` be a random message ID.
    11. Schedule a persistent task to run the following steps:
        1. (MD) Begin a transaction with scope `GROUP_SYNC` and the following
           precondition:
           1. If the group exists and is not marked as _left_, log a warning
              that a group sync race occurred and abort these steps.
        2. Let `group` be a snapshot of the current group state.
        3. Run the _Bundled Messages Send Steps_ with the following properties:
           - `id` set to `message-id`,
           - `created-at` set to the current timestamp,
           - `receivers` set to `group.members`,
           - to construct a [`group-leave`](ref:e2e.group-leave) (wrapped by
             [`group-member-container`](ref:e2e.group-member-container))
        4. (MD) Commit the transaction and await acknowledgement.

    #### Group Resync

    The following steps must be invoked when the user is the creator of a group
    and intends to resync the group manually:

    1. If the user is not the creator of the group or the group is marked as
        _left_, log an error and abort these steps.
    2. Let `message-ids` be a list of four random message IDs.
    3. Schedule a volatile task to run the following steps:
       1. (MD) Begin a transaction with scope `GROUP_SYNC` and the following
          precondition:
          1. If the group does not exist or the group is marked as _left_, log a
             warning and abort these steps.
       2. Run the _Active Group State Resync Steps_ with `message-ids` and
          `target-members` being all current group members.¹
       3. (MD) Commit the transaction and await acknowledgement.

    ¹: This mechanic intentionally bypasses the 1h _recently resynced_ mark due
    to an explicit manual request by the user to resync the group.

    #### Update Conversation

    The following steps must be invoked when the user intends to change any
    other synchronised property of the group:

    1. Let `change` be one of the following changes to the group as defined by
       `sync.Group`:
       - `notification_trigger_policy_override`
       - `notification_sound_policy_override`
       - `conversation_category`
       - `conversation_visibility`
    2. Persist the `change` to the group.
    3. (MD) Schedule a persistent task to run the following steps:
       1. Begin a transaction with scope `GROUP_SYNC` and the following precondition:
          1. If the group does not exist, log a warning and abort these steps.
       2. Reflect a `GroupSync.Update` with `group` set to contain the `change`.
       3. Commit the transaction and await acknowledgement.
       4. Persist the `change` to the group (again).

    ### Sending

    The following steps are defined as the _Bundled Messages Send Steps_ and
    must always be invoked as part of a task in order to send a message:

    1.  Let `messages` be a list of messages with each having the following
        properties:
        - `id` being the associated message ID¹,
        - `created-at` timestamp,
        - `receivers` being the set of receivers for the message²,
        - the associated conversation,
        - all necessary information to construct a _canonical_ message from it,
        - all necessary information to construct a _specific_ message from it,
          given the specific receiver.

        Note: If only one set of informations to construct a message is
        provided, this is considered both the _canonical_ and the _specific_
        message construction variant.
    2.  For each `message` of `messages`:
        1. For each `receiver` of `message.receivers`:
           1. If `receiver` is the user, log a warning, remove `receiver` from
              `receivers` and abort these sub-steps.
           2. If `receiver` is marked as _invalid_, remove `receiver` from
              `receivers and abort these sub-steps.
           3. If the properties associated to `message` to be constructed for
              `receiver` given its feature mask indicates that the message is
              not exempted from blocking, run the _Identity Blocked Steps_ for
              `receiver`'s identity. If the result indicates that `receiver` is
              blocked, remove `receiver` from `receivers` and abort these
              sub-steps.
           4. Construct the _specific_ `message` for `receiver` and attach the
              constructed message to `receiver`.
           5. Run the _Profile Picture Distribution Steps_ with the constructed
              _specific_ message for `receiver`'s type and `receiver` and extend
              `messages` with the result.
    3.  For each `message` of `messages` attach a random nonce for `message` to
        each receiver of `message.receivers`.
    4.  (MD) Let `pending-reflect-acks` be an empty list.
    5.  (MD) For each `message` of `messages`:
        1. If the properties associated to the _canonical_ `message` do not
           require reflecting outgoing messages, abort these sub-steps.
        2. Construct a `d2d.OutgoingMessage` from the _canonical_ `message` for
           the associated conversation and reflect it.
        3. Add the pending acknowledgement to `pending-reflect-acks`.
    6.  (MD) Await all `pending-reflect-acks`.
    7.  Let `pending-csp-acks` and `fs-commit-fns` be empty lists.
    8.  For each `message` of `messages`:
        1. For each `receiver` of `message.receivers`:
           1. If the constructed _specific_ message for `receiver` is of type
              `0xa0`, let `outer-messages` be a list including only the
              constructed message for `receiver` and `fs-commit-fns` be an empty
              list.
           2. If the constructed _specific_ message for `receiver` is not of
              type `0xa0`, run the _FS Encapsulation Steps_ with the constructed
              _specific_ message for `receiver` and let `outer-messages` and
              `fs-commit-fns` be the result.³
           3. For each `outer-message` of `outer-messages`:
              1. Create a `payload.message-with-metadata-box` for `outer-message`
                 with `receiver` and let `payload` be the result.
              2. If `payload.flags` does not contain the _no server
                 acknowledgement_ (`0x04`) flag, add `payload.message-id` to
                 `pending-csp-acks`.
              3. If the properties associated to `outer-message` requires
                 protection against replay, mark the nonce of `outer-message` as
                 used.
              4. Send `payload`.
    9.  Await all `pending-csp-acks`.
    10. Run each function of `fs-commit-fns`.
    11. (MD) Let `pending-reflect-acks` be an empty list.
    12. (MD) For each `message` of `messages`:
        1. If the properties associated to the _canonical_ `message` is eligible
           for reflecting `OutgoingMessageUpdate.Sent`:
           1. Create an `OutgoingMessageUpdate.Sent` for `message.id` and the
              associated conversation and reflect it.
           2. Add the pending acknowledgement to `pending-reflect-acks`.
    13. (MD) Await all `pending-reflect-acks`.
    14. For each `message` of `messages`:
        1. (MD) If the properties associated to the _canonical_ `message` was
           eligible for reflecting `OutgoingMessageUpdate.Sent`, mark it as
           _sent_ with the timestamp from the corresponding `reflect-ack`
           message.
        2. (non-MD) Mark `message` as _sent_ with the current timestamp.

    ¹: Note that, in groups, this implicitly assigns the same message ID towards
    each group member which in fact is a requirement of the protocol.

    ²: Reflecting with `receivers` empty is a legitimate case that occurs when
    sending a message in a notes group.

    ³: Always invoking the _FS Encapsulation Steps_ ensures that an FS session
    is being initiated as soon as possible, so that messages can be protected by
    FS. Moreover, it ensures that the announced FS session version is up to date
    (a newer version potentially increasing security or making more messages
    eligible for FS protection).

    The following steps are defined as the _Messages Submit Steps_ which must be
    invoked to submit a user-created message in a conversation:

    1. Let `messages` be a list of messages of the user to be added to
       the conversation with each having the following properties:
       - `id` set to a random message ID,
       - `created-at` set to the current timestamp,
       - `blobs` be an optional set of blobs that must be uploaded prior to
         being able to construct the message,
       - all necessary information to construct a _canonical_ message from it,
       - all necessary information to construct a _specific_ message from it,
         given the specific receiver.

       Note: If only one set of informations to construct a message is
       provided, this is considered both the _canonical_ and the _specific_
       message construction variant.
    2. If the associated conversation is a 1:1 conversation, run the _1:1
       Messages Submit Steps_ with `messages`.
    3. If the associated conversation is a group conversation, run the _Group
       Messages Submit Steps_ with `messages`.
    4. If the associated conversation is a distribution list conversation, run
       the _Distribution List Messages Submit Steps_ with `messages`.
    5. (Unreachable)

    The following steps are defined as the _1:1 Messages Submit Steps_ which
    must be invoked to submit a user-created message in a 1:1 conversation:

    1.  Let `messages` be a list of messages of the user to be added to
        the conversation with each having the following properties:
        - `id` defaulting to a random message ID,
        - `created-at` defaulting to the current timestamp,
        - `blobs` be an optional set of blobs that must be uploaded prior to
          being able to construct the message,
        - all necessary information to construct a _canonical_ message from it,
        - all necessary information to construct a _specific_ message from it,
          given the specific receiver.

        Note: If only one set of informations to construct a message is
        provided, this is considered both the _canonical_ and the _specific_
        message construction variant.
    2.  Let `receiver` be the conversation's associated contact.
    3.  If `receiver` has acquaintance level _deleted_, discard `messages`,
        log a warning and abort these steps.¹
    4.  Run the _Identity Blocked Steps_ for `receiver`'s identity'. If the
        result indicates that `receiver` is blocked, discard `messages`, log
        a warning and abort these steps.¹
    5.  For each `message` of `messages`, assign a random message ID to
        `message.id`.
    6.  Schedule a persistent task to run the following steps:²
        1. If the contact for `receiver` no longer exists, log an error,
           discard `messages` and abort these steps.
        2. For each `message` of `messages`:
           1. If `message.blob` is not defined, abort these sub-steps.
           2. Upload all `message.blobs` to the blob server and update
              `message` with the result (so that the message can be
              constructed).
        3. (MD) If `receiver`'s acquaintance level is not _direct_ or if at
           least one of the `messages` associated properties requires to
           unarchive the conversation and the associated conversation visibility
           is set to _archived_:
           1. Begin a transaction with scope `CONTACT_SYNC` and the following
              precondition:
              1. If the contact for `receiver` no longer exists, log an error,
                 discard `messages` and abort these steps.
           2. Let `change` be the following changes as defined by
              `sync.Contact`:
              - `acquaintance_level` set to `DIRECT`,
              - `conversation_visibility` set to `NORMAL` if the associated
                 conversation visibility is currently _archived_,
           3. Reflect a `ContactSync.Update` with `contact` set from `change`.
           4. Commit the transaction and await acknowledgement.
           5. Persist the `change` to the `receiver`.³
        4. Run the _Bundled Messages Send Steps_ for `messages` with the
           following additional properties added to each message:
           - `receivers` set to `receiver`.
    7.  If the the `receiver`'s acquaintance level is not _direct_, update it to
        _direct_.
    8.  If at least one of the `messages` associated properties requires to
        unarchive the conversation and the associated conversation visibility is
        set to _archived_, set it to _normal_.
    9.  For each `message` of `messages`, add `message` to the associated
        conversation or update a stateful message referred to by `message`
        respectively.
    10. If at least one of the `messages` associated properties requires to
        bump the conversation's _last update_ timestamp, update it to the
        current timestamp.

    ¹: While the UI should not allow to submit a message in these states,
    another device may alter the state just prior to submission, allowing to
    hit these steps in rare circumstances.

    ²: Rationale to not check for acquaintance level _deleted_ or whether the
    receiver is blocked again is the legitimate use case of the user sending a
    final message prior to blocking or removing a contact.

    ³: This is intentionally done only for MD since the user may e.g.
    immediately archive a conversation after submitting a message. This results
    in both the message and the contact sync being queued as tasks whereas in
    the non-MD case only the message task would be queued. In the MD case, the
    conversation briefly flicks back to _unarchived_ once the message has been
    sent but it immediately flicks back to _archived_ once the contact sync task
    has been executed. But because no contact sync task is created in the non-MD
    case, the message task would leave the conversation _unarchived_ which is
    not intended by the user.

    The following steps are defined as the _Group Messages Submit Steps_ which
    must be invoked to submit a user-created message in a group conversation:

    1. Let `messages` be a list of messages of the user to be added to
       the conversation with each having the following properties:
       - `id` set to a random message ID,
       - `created-at` set to the current timestamp,
       - `blobs` be an optional set of blobs that must be uploaded prior to
         being able to construct the message,
       - all necessary information to construct a _canonical_ message from it,
       - all necessary information to construct a _specific_ message from it,
         given the specific receiver.

       Note: If only one set of informations to construct a message is provided,
       this is considered both the _canonical_ and the _specific_ message
       construction variant.
    2. If the group is marked as _left_, discard `messages`, log a warning and
       abort these steps.¹
    3. For each `message` of `messages`, assign a random message ID to
       `message.id`.
    4. Schedule a persistent task to run the following steps:²
       1. If the group does not exist or is marked as _left_, log a warning,
          discard `messages` and abort these steps.
       2. For each `message` of `messages`:
          1. If `message.blob` is not defined, abort these sub-steps.
          2. Upload all `message.blobs` to the blob server with the _persist_
             flag and update `message` with the result (so that the message
             can be constructed).
       3. (MD) If at least one of the `messages` associated properties requires
          to unarchive the conversation and the associated conversation
          visibility is set to _archived_:
          1. Begin a transaction with scope `GROUP_SYNC` and the following
             precondition:
             1. If the group does not exist or is marked as left, log a warning
                that a group sync race occurred, discard `messages` and abort
                these steps.
          2. Let `change` be the following changes as defined by `sync.Group`:
             - `conversation_visibility` set to `NORMAL` if the associated
               conversation visibility is currently _archived_,
          3. Reflect a `GroupSync.Update` with `group` set from `change`.
          4. Commit the transaction and await acknowledgement.
          5. Persist the `change` to the group.³
       4. Run the _Bundled Messages Send Steps_ for `messages` with the
          following additional properties added to each message:
          - `receivers` set to the group's members.
    5. If at least one of the `messages` associated properties requires to
       unarchive the conversation and the associated conversation visibility is
       set to _archived_, set it to _normal_.
    6. For each `message` of `messages`, add `message` to the associated
       conversation or update a stateful message referred to by `message`
       respectively.
    7. If at least one of the `messages` associated properties requires to
       bump the conversation's _last update_ timestamp, update it to the current
       timestamp.

    ¹: While the UI should not allow to submit a message in these states,
    another device may alter the state just prior to submission, allowing to
    hit these steps in rare circumstances.

    ²: Rationale to not check for acquaintance level _deleted_ or whether the
    receiver is blocked again is the legitimate use case of the user sending a
    final message prior to removing a group.

    ³: This is intentionally done only for MD since the user may e.g.
    immediately archive a conversation after submitting a message. This results
    in both the message and the group sync being queued as tasks whereas in the
    non-MD case only the message task would be queued. In the MD case, the
    conversation briefly flicks back to _unarchived_ once the message has been
    sent but it immediately flicks back to _archived_ once the group sync task
    has been executed. But because no group sync task is created in the non-MD
    case, the message task would leave the conversation _unarchived_ which is
    not intended by the user.

    The following steps are defined as the _Distribution List Messages Submit
    Steps_ which must be invoked to submit a user-composed message in a
    distribution list conversation:

    1. Let `messages` be a list of messages of the user to be added to
       the conversation with each having the following properties:
       - `id` set to a random message ID,
       - `created-at` set to the current timestamp,
       - `blobs` be an optional set of blobs that must be uploaded prior to
         being able to construct the message,
       - all necessary information to construct a _canonical_ message from it,
       - all necessary information to construct a _specific_ message from it,
         given the specific receiver.

       Note: If only one set of informations to construct a message is
       provided, this is considered both the _canonical_ and the _specific_
       message construction variant.
    2. For each `message` of `messages`, assign a random message ID to
       `message.id`.
    3. Schedule a persistent task to run the following steps:
       1. If the distribution list does not exist, log a warning, discard
          `messages` and abort these steps.
       2. For each `message` of `messages`:
          1. If `message.blob` is not defined, abort these sub-steps.
          2. Upload all `message.blobs` to the blob server with the _persist_
             flag and update `message` with the result (so that the message
             can be constructed).
       3. (non-MD) Let `members` be all current members of the distribution
          list. Remove any members with acquaintance level _deleted_ from
          `members`.¹
       4. (MD) If at least one of the `messages` associated properties requires
          to unarchive the conversation and the associated conversation
          visibility is set to _archived_:
          1. Begin a transaction with scope `DISTRIBUTION_LIST_SYNC` and the
             following precondition:
             1. If the distribution list does not exist, log a warning that a
                distribution list sync race occurred, discard `messages` and
                abort these steps.
          2. Let `members` be all current members of the distribution list.
             Remove any members with acquaintance level _deleted_ from `members`.¹
          3. Let `change` be the following changes as defined by
             `sync.DistributionList`:
             - `member_identities` from `members`,
             - `conversation_visibility` set to `NORMAL` if the associated
               conversation visibility is currently _archived_,
          4. Reflect a `DistributionList.Update` with `distribution_list` set from `change`.
          5. Commit the transaction and await acknowledgement.
          6. Persist the `change` to the distribution list.²
       5. Run the _Bundled Messages Send Steps_ for `messages` with the
          following additional properties added to each message:
          - `receivers` from `members`.
    4. If at least one of the `messages` associated properties requires to
       unarchive the conversation and the associated conversation visibility is
       set to _archived_, set it to _normal_.
    5. For each `message` of `messages`, add `message` to the associated
       conversation or update a stateful message referred to by `message`
       respectively.
    6. If at least one of the `messages` associated properties requires to
       bump the conversation's _last update_ timestamp, let `last-update-at` be
       the current timestamp.
    7. If `last-update-at` is defined, update the associated _last update_
       timestamp of the conversation to `last-update-at`.
    8. For each `member` of the distribution list:
       1. Add each message of `messages` to the 1:1 conversation associated to
          `member`.
       2. If `last-update-at` is defined, update the associated _last update_
          timestamp of the 1:1 conversation of `member` to `last-update-at`.

    ¹: There should be no receivers in a distribution list that have
    acquaintance level _deleted_, so the filtering is only done for safety.

    2: This is intentionally done only for MD since the user may e.g.
    immediately archive a conversation after submitting a message. This results
    in both the message and the distribution list sync being queued as tasks
    whereas in the non-MD case only the message task would be queued. In the MD
    case, the conversation briefly flicks back to _unarchived_ once the message
    has been sent but it immediately flicks back to _archived_ once the
    distribution list sync task has been executed. But because no distribution
    list sync task is created in the non-MD case, the message task would leave
    the conversation _unarchived_ which is not intended by the user.

    ### Receiving

    The following steps are defined as _Common Group Receive Steps_ and will
    be applied in most cases for group messages:

    1. Look up the group.
    2. If the group could not be found:
       1. If the user is not the creator of the group, run the _Group Sync
          Request Steps_.
       2. Discard the message and abort these steps.
    3. If the group is marked as _left_:
       1. If the user is the creator of the group, run the _Bundled Messages
          Send Steps_ with the following properties:
          - `id` set to a random message ID,
          - `created-at` set to the current timestamp,
          - `receivers` set to the sender,
          - to construct a
            [`group-setup`](ref:e2e.group-setup) (wrapped by
            [`group-creator-container`](ref:e2e.group-creator-container)) with
            an empty members set
       2. If the user is not the creator of the group, run the _Bundled
          Messages Send Steps_ with the following properties:
          - `id` set to a random message ID,
          - `created-at` set to the current timestamp,
          - `receivers` set to the sender,
          - to construct a
            [`group-leave`](ref:e2e.group-leave) (wrapped by
            [`group-member-container`](ref:e2e.group-member-container))
       3. Discard the message and abort these steps.
    4. If the sender is not a member of the group:
       1. If the user is the creator of the group, run the _Bundled Messages
          Send Steps_ with the following properties:
          - `id` set to a random message ID,
          - `created-at` set to the current timestamp,
          - `receivers` set to the sender,
          - to construct a
            [`group-setup`](ref:e2e.group-setup) (wrapped by
            [`group-creator-container`](ref:e2e.group-creator-container)) with
            an empty members set
       2. If the user is not the creator of the group, run the _Group Sync
          Request Steps_, then discard the message and abort these steps.
       3. Discard the message and abort these steps.

    This rule and any exceptions will be referenced/defined explicitly for each
    message.

    Note that steps are not allowed to discard messages from blocked contacts
    prior to running these steps if the message alters group state (group
    control messages), or is stateful (i.e. introduces a poll, poll vote, or a
    group call).

    ### Periodic Group Sync

    When the creator of a group...

    - is about to send a group conversation message, or
    - did just receive a group conversation message,

    it must trigger a _group sync_ for this group if the last time the
    _group sync_ was triggered is more than seven days ago.

    When a _group sync_ is triggered, the creator assumes it has received a
    [`group-sync-request`](ref:e2e.group-sync-request) from every group member
    and must now respond accordingly to each member of the group.

    A newly created group counts as an initial _group sync_ trigger. In other
    words, the first group sync of a newly created group triggers seven days
    in the future when one of the above described conditions is met.

    This provides a form of self-healing in case a device lost its group state
    (e.g. due to a backup restore) and was unable to correct this mischief.

    [//]: # "TODO(SE-40): Group states"

    ### Blobs

    Since messages have a strict maximum size limitation, large binary blobs
    are uploaded to the blob server. Blobs currently have a maximum size of
    50 MiB.

    When Multi-Device is activated, all Blobs must be downloaded via the
    respective Blob Mirror unless explicitly stated otherwise.

    #### Upload

    The following steps are defined as the _Blob Upload Steps_:

    1. Let `blob` be the following properties:
       - `data` being the encrypted binary data to be uploaded,
       - `scope` being either _public_ (for public facing blobs) or _local_ (for
           device group facing blobs).
       - `persist` being a mark (primarily for usage within groups).
    2. (non-MD) Upload to the blob server from `blob`.
    3. (MD) Upload to the blob mirror server from `blob` and the device group
       information.
    4. If the upload stream stalls for more than 10s, exceptionally abort these
       steps.
    5. Return the resulting blob ID.

    #### Download

    The following steps are defined as the _Blob Download Steps_:

    1. Let `blob` be the following properties:
       - `id` being the blob ID,
       - `scope` being either _public_ (for public facing blobs) or _local_ (for
           device group facing blobs).
    2. (non-MD) Download the blob data from the blob server using `blob`.
    3. (MD) Download the blob data from the blob mirror server using `blob` and
       the device group information.
    4. If the download stream stalls for more than 10s, exceptionally abort
       these steps.
    5. Schedule a volatile background task to run the following steps:
       1. (non-MD) Request to mark the blob download from the blob server of
          `blob.id` as _done_.
       2. (MD) Request to mark the blob download from the blob mirror server of
          `blob.id` as _done_ with the device group information.
       3. If no response could be obtained within 10s or the request was
          unsuccessful, log a warning.
    6. Return the resulting blob data.

    ### Image, Audio, Video vs. File

    Images, as well as audio and video sources can be either send as special
    media messages or as files. When sending as a file, i.e. a
    [`file`](ref:e2e.file) message struct with rendering type `0x00` (file), no
    transcoding is necessary and no media type restrictions apply.

    Clients should intelligently choose between a media message and a file
    message but always leave the final choice to the user.

    The following sections describe what restrictions apply and modifications
    need to be made in case the source is sent as a media message, i.e. one
    of the specialised (deprecated) media structs or a [`file`](ref:e2e.file)
    message struct with rendering type `0x01` (media) or `0x02` (sticker).

    ### Images

    Images must be in JPEG format for the legacy
    [`deprecated-image`](ref:e2e.deprecated-image) message and for profile
    pictures.

    When using the [`file`](ref:e2e.file) message struct, the following media
    types are explicitly supported:

    - image/gif
    - image/jpeg
    - image/png
    - image/webp

    The following media types are explicitly not supported:

    - image/svg+xml

    Other media types _may_ be supported.

    Keep the format when resizing images or creating thumbnails, if possible
    (e.g. if the source is a JPEG, make the thumbnail a JPEG). When the format
    cannot be kept, use PNG for source images with transparency or
    lossless encoding (e.g. screenshots) and JPEG for images without
    transparency or lossy encoding (e.g. photos).

    Recommended maximum dimensions:

    - Small: 640x640
    - Medium: 1024x1024
    - Large: 1600x1600
    - Extra Large: 2592x2592
    - Original: As is

    ### Thumbnails

    Apply the logic described for images to all thumbnails with recommended
    maximum dimensions of 512x512.

    ### User Profile Distribution

    The shareable part of the user profile consists of the user's public
    nickname and the profile picture:

    - The nickname is sent along with outgoing messages as `sender-nickname`
      inside the [`legacy-message`](ref:payload.legacy-message) or as part of
      the metadata in
      [`message-with-metadata-box`](ref:payload.message-with-metadata-box).
    - The profile picture is distributed as described in
      [Profile Picture Distribution](ref:e2e#profile-picture-distribution).

    Whether user profile distribution should be triggered by an outgoing message
    is specified in the description of every message type below.

    ### Profile Pictures

    Apply the logic described for images to all profile pictures with
    recommended maximum dimensions of 512x512 with a square aspect ratio.

    #### Profile Picture Distribution

    Every time a message is being sent to a specific contact or a group of
    contacts, the sender needs to evaluate whether the profile picture needs to
    be sent. If the receiver of the message is a group, the evaluation needs to
    be done for each contact of that group.

    The following steps are defined as the _Profile Picture Distribution Steps_:

    1.  Let `type` be a message type associated to a message that is about to be
        sent.
    2.  Let `receiver` be the receiver of the message.
    3.  If the properties associated to `type` do not require _User Profile
        Distribution_, abort these steps without a message.
    4.  If `receiver`'s Threema ID is `ECHOECHO` or a Threema Gateway ID (starts
        with a `*`), abort these steps without a message.
    5.  Let `cache` be the most recently distributed profile picture message
        variant towards `receiver`, being either
        - a blob ID if the user's profile picture was distributed via a
          [`set-profile-picture`](ref:e2e.set-profile-picture) message, or
        - a _remove_ mark and a timestamp if the user's profile picture was
          removed via a
          [`delete-profile-picture`](ref:e2e.delete-profile-picture) message.
    6.  If the user has no profile picture or the settings indicate that the
        user's profile picture should be shared with nobody or the settings
        associated to `receiver` indicate that the profile picture should not be
        distributed to it:
        1. If `cache` is a _remove_ mark and indicates that the most recent
           [`delete-profile-picture`](ref:e2e.delete-profile-picture) message
           towards `receiver` was sent less than seven days ago, abort these
           steps without a message.
        2. Update the `cache` for `receiver` to a _remove_ mark with the current
           timestamp.
        3. Return the following properties:
           - `id` being a random message ID,
           - `created-at` set to the current timestamp,
           - `receivers` set to `receiver`,
           - to construct a
           [`delete-profile-picture`](ref:e2e.delete-profile-picture).
    7.  If there is a cached profile picture of the user and the associated blob
        was uploaded more than seven days ago, remove the cached profile
        picture.
    8.  If `cache` contains a blob ID of the most recent
        [`set-profile-picture`](ref:e2e.set-profile-picture) message sent
        towards `receiver` that equals the blob ID of the cached profile
        picture, abort these steps without a message.
    9.  If there is no cached profile picture, encrypt the user's profile
        picture with a random symmetric key and upload it to the blob server.
        Store the key and the resulting blob ID as the cached profile picture of
        the user.
    10. Update the `cache` for `receiver` to the blob ID of the cached profile
        picture.
    11. Return the following properties:
        - `id` being a random message ID,
        - `created-at` set to the current timestamp,
        - `receivers` set to `receiver`,
        - to construct a [`set-profile-picture`](ref:e2e.set-profile-picture)
          message using the cached profile picture's blob ID and key.

    When the user changes the profile picture, run the steps associated to
    update the user's profile with the new profile picture or the newly removed
    profile picture.

    #### Profile Picture Sharing Settings

    In the client settings, there are three profile picture sharing options that
    the user can choose from:

    - Share with nobody
    - Share with everybody you write to
    - Share with selected contacts only

    The default is to share the profile picture with everyone.

    #### Contact Profile Picture Precedence

    There are three different sources of profile pictures, ordered by
    precedence:

    1. _contact-defined_: Set by the contact, distributed through a
       [`set-profile-picture`](ref:e2e.set-profile-picture) message.
    2. _gateway-defined_: Set by the creator in the Threema Gateway (or Threema
       Broadcast) control panel and distributed through `avatar.threema.ch`.
       Only applicable to Threema Gateway IDs (starting with a `*`).
    3. _user-defined_: Set by the app user for this contact or imported from
       the address book. Applicable to all Threema IDs which are not Threema
       Gateway IDs.

    The following steps are defined as _Contact Profile Picture Selection Steps_
    and will be applied to determine the contact's profile picture that should
    be displayed:

    1. Let `id` be the Threema ID of the contact.
    2. If the _contact-defined_ picture is set for the contact, apply it and
       abort these steps.
    3. If `id` starts with a `*` (is a Threema Gateway ID) and the
       _gateway-defined_ picture is set for the contact, apply it and abort
       these steps.
    4. If `id` does not start with `*` and the _user-defined_ picture is set
       for the contact, apply it and abort these steps.
    5. Apply a fallback picture.

    #### Recurring Gateway Contact Profile Picture Refresh

    For contacts with a Threema Gateway ID (starting with a `*`), the profile
    picture needs to be fetched recurringly:

    1. Fetch the profile picture for the ID from `avatar.threema.ch`.
    2. If no profile picture could be found, schedule the next refresh in 24h
       and abort these steps.
    3. Store the profile picture as the _gateway-defined_ picture.
    4. Schedule the next refresh according to the `expires` header of the HTTP
       response.
    5. Run the _Contact Profile Picture Selection Steps_ for this contact.

    ### Audio

    Audio must be in AAC format.

    If the source is already in AAC, no transcoding is necessary. Otherwise,
    the recommended transcoding settings are: Bitrate 128 kbit/s, 2 channels.

    When recording audio (i.e. a voice message), the recommended recording
    settings are: Sample rate 44.1 kHz, bitrate 32 kbit/s, 1 channel.

    ### Video

    Videos must be encoded in H.264 and the MP4 container format.

    Recommended encoding settings for all videos:

    - Low: 480x480, scale by maintaining aspect ratio to nearest multiple
      of 16px. Video bitrate 384 kbit/s, audio bitrate 32 kbit/s (2
      channels). Baseline Profile, Level 3.1.
    - High: 848x848, scale by maintaining aspect ratio to nearest multiple
      of 16px. Video bitrate 1500 kbit/s, audio bitrate 64 kbit/s (2
      channels). Baseline Profile, Level 3.1.
    - Original: As is. Still needs transcoding in case a different codec has
      been used.

    When recording a video, the following recording settings are recommended
    to avoid post-reencoding: 1280x720 / 720x1280. Video bitrate 2000 kbit/s
    at 30 fps, audio bitrate 128 kbit/s (2 channels).

    ### Call Features

    Call features are transmitted within either a
    [`call-offer`](ref:e2e.call-offer) or a [`call-answer`](ref:e2e.call-answer)
    message. It is an optional object containing the below defined fields. If
    the object is not provided, assume an empty features object.

    - Video Support (`'video'`): Set this field to `null` or an empty object if
      video calls are enabled. If either side omits this field, video support
      is disabled for the upcoming call.

    ### Application Setup

    The following steps are defined as _Application Setup Steps_ and must be run
    when a new Threema ID has been created or when application state has been
    restored from a backup:

    1. [...]
    2. If application state has not been set up by the _Device Join Protocol_
       (meaning that multi-device is deactivated):
       1.  [...]
       2.  Update the user's feature mask on the directory server.
       3.  Let `contacts` be the list of all contacts, including those with an
           acquaintance level different than _direct_.
       4.  Refresh the state, type and feature mask of all `contacts` from the
           directory server and make any changes persistent.
       5.  Let `solicited-contacts` be a copy of `contacts` filtered in the
           following way. For each `contact`:
           1. If the `contact`'s activity state is _invalid_ (i.e. it does not
              exist or has been revoked), remove `contact` from the list and
              abort these sub-steps.
           2. If `contact` is part of a group that is not marked as _left_, add
              `contact` to the list and abort these sub-steps.
           3. Lookup the 1:1 conversation with `contact` and let `last-update`
              be the associated _last update_ timestamp.
           4. If `last-update` is defined, add `contact` to the list and abort
              these sub-steps.
           5. Remove `contact` from the list.
       6.  If FS is supported by the client, run the _FS Refresh Steps_ with
           `solicited-contacts`.
       7.  For each `contact` of `solicited-contacts` run the _Bundled Messages
           Send Steps_ with the following properties:
           - `id` being a random message ID,
           - `created-at` set to the current timestamp,
           - `receivers` set to `contact`,
           - to construct a
             [`contact-request-profile-picture`](ref:e2e.contact-request-profile-picture)
       8.  For each group not marked as _left_:
           1. If the user is the creator of the group, trigger a _group sync_
              for that group.
           2. If the user is not the creator of the group, run the _Group Sync
              Request Steps_ for the group.
       9. [...]
    3. Commit the application state and exit the setup phase.

    ### Application Update

    The following steps are defined as _Application Update Steps_ and must be
    run as a persistent task when the application has just been updated to a new
    version or downgraded to a previous version:

    1. [...]
    2. Update the user's feature mask on the directory server.
    3. Let `contacts` be the list of all contacts (regardless of the
       acquaintance level).
    4. Refresh the state, type and feature mask of all `contacts` from the
       directory server and make any changes persistent.
    5. For each `contact` of `contacts`:
       1. If an associated FS session with `contact` exists and any of the FS
          states is unknown or any of the stored FS versions (local or remote)
          is unknown, terminate the FS session by running the _Bundled Messages
          Send Steps_ with the following properties:
          - `id` being a random message ID,
          - `created-at` set to the current timestamp,
          - `receivers` set to `contact`,
          - to construct a `csp-e2e-fs.Terminate` message with cause `RESET`.
    6. [...]

    Note: Reactivation of FS due to disabling multi-device should run the
    _Application Setup Steps_ step 2.2. through 2.6. TODO(SE-199): This note
    will be removed once multi-device supports FS.

  container:
    _group: Header
    _doc: |-
      Contains an end-to-end encrypted message.
    fields:
      - _doc: |-
          Type of the message (`common.CspE2eMessageType`).
        name: type
        type: u8
      - _doc: |-
          Inner message. Needs to be parsed according to the `type` field.
          Padded with a random amount from 1 to 255 bytes in [PKCS#7
          format](https://datatracker.ietf.org/doc/html/rfc5652#section-6.3).

          Additionally, for security reasons, the total size of `padded-data`
          should be at least 32 bytes, to avoid leaking information about the
          contents.

          Example padding (hex representation):

          - 1 byte: `01`
          - 3 bytes: `030303`
          - 10 bytes: `0A0A0A0A0A0A0A0A0A0A`

          To add padding without information leaks, run the following steps:

          1. Let `data` be the data to be padded.
          2. Let `pad-length` be a random number between (inclusive) 1 and 255.
          3. If the sum of the byte length of `data` and `pad-length` is less
             than 32, update `pad-length` so the sum is precisely 32.
          4. Let `pad-byte` be the encoded unsigned 8-bit integer
             representation of `pad-length`.
          5. Let `padded-data` be the padded data by adding `pad-length`
             trailing `pad-byte` bytes to `data`.

          To remove padding:

          1. Let `pad-length` be the decoded unsigned 8-bit integer
             representation of the last byte of `padded-data`.
          2. Let `data` be the unpadded data by ignoring the trailing
             `pad-length` bytes of `padded-data`.
        name: padded-data
        type: b*

  group-creator-container:
    _group: Header
    _doc: |-
      Container that is wrapped around some special group messages sent by the
      creator to normal group members and vice versa.
    fields:
      - _doc: |-
          8 byte random group ID. Uniquely identifies the group when combined
          with the creator's Threema ID.
        name: group-id
        type: *group-id
      - _doc: |-
          Inner message struct.
        name: inner-data
        type: b*

  group-member-container:
    _group: Header
    _doc: |-
      Container that is wrapped around most messages sent by group members to
      other group members.
    fields:
      - _doc: |-
          The group creator's Threema ID.
        name: creator-identity
        type: *identity
      - _doc: |-
          8 byte random group ID assigned to the group by the creator.
        name: group-id
        type: *group-id
      - _doc: |-
          Inner message struct.
        name: inner-data
        type: b*

  empty:
    _doc: |-
      An empty message (duh).

      Only used when encapsulated by an `csp_e2e_fs.Envelope` to announce a new
      FS version without explicit renegotiation.

      **Properties**:
      - Kind: 1:1
      - Flags: None
      - User profile distribution: No
      - Exempt from blocking: Yes
      - Implicit _direct_ contact creation: No
      - Protect against replay: Yes
      - Unarchive: No
      - Bump _last update_: No
      - Reflect:
        - Incoming: No
        - Outgoing: No
        - _Sent_ update: No
      - Delivery receipts: No
      - Reactions: No
      - When rejected: N/A (ignored)
      - Send to Threema Gateway ID group creator: N/A

      When the user submits this message in a 1:1, group or distribution list,
      _Rick Astley - Never Gonna Give You Up_ must be played and looped
      indefinitely on the user's device.

      When receiving this message, a mechanical finger must be unlatched from
      the device and boop the receiving user on the nose.

  text:
    _group: Conversation Messages
    _doc: |-
      A text message.

      **Properties (1:1)**:
      - Kind: 1:1
      - Flags:
        - `0x01`: Send push notification.
      - User profile distribution: Yes
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: Yes
      - Protect against replay: Yes
      - Unarchive: Yes
      - Bump _last update_: Yes
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes
        - _Sent_ update: Yes
      - Delivery receipts: Yes
      - Reactions: Yes
      - When rejected: Re-send after confirmation
      - Edit applies to: Text
      - Deletable by: User and sender
      - Send to Threema Gateway ID group creator: N/A

      **Properties (Group)**:
      - Kind: Group
      - Flags:
        - `0x01`: Send push notification.
      - User profile distribution: Yes
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: No
      - Protect against replay: Yes
      - Unarchive: Yes
      - Bump _last update_: Yes
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes
        - _Sent_ update: Yes
      - Delivery receipts: N/A
      - Reactions: Yes
      - When rejected: Re-send after confirmation
      - Edit applies to: Text
      - Deletable by: User and sender
      - Send to Threema Gateway ID group creator: If capture is enabled

      When the user submits this message in a 1:1, group or distribution list
      conversation:

      1. Split the provided text into multiple segments with at most 6000 UTF-8
         encoded bytes while preserving grapheme clusters at boundaries and let
         `text-segments` be the result.¹
      2. Run the _Messages Submit Steps_ with `messages` set from
         `text-segments` (for a group, wrapped by
         [`group-member-container`](ref:e2e.group-member-container)).

      ¹: The UI may warn the user that the message will be split if the UTF-8
      encoded text exceeds 6000 bytes and request confirmation before
      submission.

      When reflected from another device as an incoming or outgoing 1:1
      message:

      1. Add the message to the associated 1:1 conversation.

      When receiving this message as a 1:1 message:

      1. Add the message to the associated 1:1 conversation.

      When reflected from another device as an incoming or outgoing group
      message (wrapped by
      [`group-member-container`](ref:e2e.group-member-container)):

      1. Add the message to the associated group conversation.

      When receiving this message as a group message (wrapped by
      [`group-member-container`](ref:e2e.group-member-container)):

      1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the
         message has been discarded, abort these steps.
      2. Add the message to the associated group conversation.
    fields:
      - _doc: |-
          UTF-8 encoded text.
        name: text
        type: b*

  deprecated-image:
    _group: Conversation Messages
    _doc: |-
      An image message.

      Note: This message is deprecated and may be phased out eventually. When
      sending images, use the [`file`](ref:e2e.file) message with the rendering
      type `0x01` (media).

      **Properties**:
      - Kind: 1:1
      - Flags:
        - `0x01`: Send push notification.
      - User profile distribution: Yes
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: Yes
      - Protect against replay: Yes
      - Unarchive: Yes
      - Bump _last update_: Yes
      - Reflect:
        - Incoming: Yes
        - Outgoing: N/A
        - _Sent_ update: Yes
      - Delivery receipts: Yes
      - Reactions: Yes
      - When rejected: N/A (deprecated message is not being sent)
      - Edit applies to: N/A
      - Deletable by: User and sender
      - Send to Threema Gateway ID group creator: N/A

      The image must be in JPEG format, is uploaded to the blob server and
      encrypted by:

          XSalsa20-Poly1305(
            key=X25519HSalsa20(<sender.CK>.secret, <receiver.CK>.public),
            nonce=<deprecated-image.nonce>,
          )

      This message is deprecated and may no longer be submitted.

      When reflected from another device as an incoming message:

      1. Run the _Common Deprecated Image Receive Steps_.

      When receiving this message:

      1. Run the _Common Deprecated Image Receive Steps_.

      The following steps are defined as the _Common Deprecated Image Receive
      Steps_:

      1. Add the message to the associated 1:1 conversation.
      2. If this message is eligible for auto-download, schedule downloading the
         image data from the blob server and request the blob to be removed.
    fields:
      - _doc: |-
          Blob ID to obtain the image data.
        name: image-blob-id
        type: *blob-id
      - _doc: |-
          Image size in bytes.
        name: image-size
        type: u32-le
      - _doc: |-
          Random nonce used to encrypt the image data.
        name: nonce
        type: *nonce

  deprecated-group-image:
    _group: Conversation Messages
    _doc: |-
      An image message (only used by groups).

      Note: This message is deprecated and may be phased out eventually. When
      sending images, use the [`file`](ref:e2e.file) message with the rendering
      type `0x01` (media).

      **Properties**:
      - Kind: Group
      - Flags:
        - `0x01`: Send push notification.
      - User profile distribution: Yes
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: No
      - Protect against replay: Yes
      - Unarchive: Yes
      - Bump _last update_: Yes
      - Reflect:
        - Incoming: Yes
        - Outgoing: N/A
        - _Sent_ update: Yes
      - Delivery receipts: N/A
      - Reactions: Yes
      - When rejected: N/A (deprecated message is not being sent)
      - Edit applies to: N/A
      - Deletable by: User and sender
      - Send to Threema Gateway ID group creator: If capture is enabled

      The image must be in JPEG format, is uploaded to the blob server and
      encrypted by:

          XSalsa20-Poly1305(key=<deprecated-group-image.key>, nonce=00..01)

      This message is deprecated and may no longer be submitted.

      When reflected from another device as an incoming message (wrapped by
      [`group-member-container`](ref:e2e.group-member-container)):

      1. Run the _Common Deprecated Group Image Receive Steps_.

      When receiving this message (wrapped by
      [`group-member-container`](ref:e2e.group-member-container)):

      1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the
         message has been discarded, abort these steps.
      2. Run the _Common Deprecated Group Image Receive Steps_.

      The following steps are defined as the _Common Deprecated Group Image
      Receive Steps_:

      1. Add the message to the associated group conversation.
      2. If this message is eligible for auto-download, schedule downloading the
         image data from the blob server but do not request the blob to be
         removed.
    fields:
      - _doc: |-
          Blob ID to obtain the image data.
        name: image-blob-id
        type: *blob-id
      - _doc: |-
          Image size in bytes.
        name: image-size
        type: u32-le
      - _doc: |-
          Random symmetric key used to encrypt the image data.
        name: key
        type: *key

  location:
    _group: Conversation Messages
    _doc: |-
      A location message.

      **Properties (1:1)**:
      - Kind: 1:1
      - Flags:
        - `0x01`: Send push notification.
      - User profile distribution: Yes
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: Yes
      - Protect against replay: Yes
      - Unarchive: Yes
      - Bump _last update_: Yes
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes
        - _Sent_ update: Yes
      - Delivery receipts: Yes
      - Reactions: Yes
      - When rejected: Re-send after confirmation
      - Edit applies to: N/A
      - Deletable by: User and sender
      - Send to Threema Gateway ID group creator: N/A

      **Properties (Group)**:
      - Kind: Group
      - Flags:
        - `0x01`: Send push notification.
      - User profile distribution: Yes
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: No
      - Protect against replay: Yes
      - Unarchive: Yes
      - Bump _last update_: Yes
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes
        - _Sent_ update: Yes
      - Delivery receipts: N/A
      - Reactions: Yes
      - When rejected: Re-send after confirmation
      - Edit applies to: N/A
      - Deletable by: User and sender
      - Send to Threema Gateway ID group creator: If capture is enabled

      When the user submits this message in a 1:1, group or distribution list
      conversation:

      1. Let `location` be the provided location with the following properties:
         - `latitude` being a WGS-84 string,
         - `longitude` being a WGS-84 string,
         - `accuracy` being a floating point number or undefined,
         - `address` being an address of a point of interest or undefined,
         - `name` being a name for the point of interest or undefined,
      2. If `location.name` is defined but `location.address` is not defined,
         discard `location`, log an error and abort these steps.¹
      3. If the UTF-8 encoded bytes of `location.address` or `location.name`
         exceed 512 bytes, discard `location`, log an error and abort these
         steps.¹
      4. Run the _Messages Submit Steps_ with `messages` set from `location`
         (for a group, wrapped by
         [`group-member-container`](ref:e2e.group-member-container)).

      ¹: The UI should prevent submission in these cases.

      When reflected from another device as an incoming or outgoing 1:1
      message:

      1. Add the message to the associated 1:1 conversation.

      When receiving this message as a 1:1 message:

      1. Add the message to the associated 1:1 conversation.

      When reflected from another device as an incoming or outgoing group
      message (wrapped by
      [`group-member-container`](ref:e2e.group-member-container)):

      1. Add the message to the associated group conversation.

      When receiving this message as a group message (wrapped by
      [`group-member-container`](ref:e2e.group-member-container)):

      1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the
         message has been discarded, abort these steps.
      2. Add the message to the associated group conversation.
    fields:
      - _doc: |-
          Location coordinates and meta information encoded in comma- and
          line-separated UTF-8:

              <latitude>,<longitude>[,<accuracy>]

          or

              <latitude>,<longitude>[,<accuracy>]
              <address>

          or

              <latitude>,<longitude>[,<accuracy>]
              <name>
              <address>

          Values:

          - `latitude` and `longitude` are the geographic coordinates
            represented in a WGS-84 string.
          - `accuracy` is the accuracy in meters represented by a floating
            point number formatted as a string.
          - `address` is a full address. If it contains multiple lines, each
            line feed must be escaped (literal `\n`).
          - `name` is the name of a point of interest.

          _Latitude_ and _longitude_ must always be present while _accuracy_
          is optional and should only be provided when the current location of
          the device is being sent. These values are comma-separated.

          Following values are optional and separated by line feeds (`\n`).
          This may be either:

          - a single line containing the _address_ representing the closest
            address matching the coordinates, or
          - two lines containing a point of interest _name_ and _address_
            (which means that the coordinates refer to the point of interest).
        name: location
        type: b*

  deprecated-audio:
    _group: Conversation Messages
    _doc: |-
      An audio message.

      Note: This message is deprecated and may be phased out eventually. When
            sending audio, use the [`file`](ref:e2e.file) message with the
            rendering type `0x01` (media).

      **Properties (1:1)**:
      - Kind: 1:1
      - Flags:
        - `0x01`: Send push notification.
      - User profile distribution: Yes
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: Yes
      - Protect against replay: Yes
      - Unarchive: Yes
      - Bump _last update_: Yes
      - Reflect:
        - Incoming: Yes
        - Outgoing: N/A
        - _Sent_ update: Yes
      - Delivery receipts: Yes
      - Reactions: Yes
      - When rejected: N/A (deprecated message is not being sent)
      - Edit applies to: N/A
      - Deletable by: User and sender
      - Send to Threema Gateway ID group creator: N/A

      **Properties (Group)**:
      - Kind: Group
      - Flags:
        - `0x01`: Send push notification.
      - User profile distribution: Yes
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: No
      - Protect against replay: Yes
      - Unarchive: Yes
      - Bump _last update_: Yes
      - Reflect:
        - Incoming: Yes
        - Outgoing: N/A
        - _Sent_ update: Yes
      - Delivery receipts: N/A
      - Reactions: Yes
      - When rejected: N/A (deprecated message is not being sent)
      - Edit applies to: N/A
      - Deletable by: User and sender
      - Send to Threema Gateway ID group creator: If capture is enabled

      The audio is uploaded to the blob server and encrypted by:

          XSalsa20-Poly1305(key=<deprecated-audio.key>, nonce=00..01)

      This message is deprecated and may no longer be submitted.

      When reflected from another device as an incoming 1:1 message:

      1. Run the _Common Deprecated Audio Receive Steps_.

      When receiving this message as a 1:1 message:

      1. Run the _Common Deprecated Audio Receive Steps_.

      When reflected from another device as an incoming group message (wrapped
      by [`group-member-container`](ref:e2e.group-member-container)):

      1. Run the _Common Deprecated Audio Receive Steps_.

      When receiving this message as a group message (wrapped by
      [`group-member-container`](ref:e2e.group-member-container)):

      1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the
         message has been discarded, abort these steps.
      2. Run the _Common Deprecated Audio Receive Steps_.

      The following steps are defined as the _Common Deprecated Audio Receive
      Steps_:

      1. Add the message to the associated conversation.
      2. If this message is eligible for auto-download, schedule downloading the
         audio data from the blob server. Only request the blob to be removed if
         the associated conversation is a 1:1 conversation.
    fields:
      - _doc: |-
          Audio duration in seconds.
        name: duration
        type: u16-le
      - _doc: |-
          Blob ID to obtain the audio data.
        name: audio-blob-id
        type: *blob-id
      - _doc: |-
          Audio size in bytes.
        name: audio-size
        type: u32-le
      - _doc: |-
          Random symmetric key used to encrypt the audio data.
        name: key
        type: *key

  deprecated-video:
    _group: Conversation Messages
    _doc: |-
      A video message.

      Note: This message is deprecated and may be phased out eventually. When
            sending video, use the [`file`](ref:e2e.file) message with the
            rendering type `0x01` (media).

      **Properties (1:1)**:
      - Kind: 1:1
      - Flags:
        - `0x01`: Send push notification.
      - User profile distribution: Yes
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: Yes
      - Protect against replay: Yes
      - Unarchive: Yes
      - Bump _last update_: Yes
      - Reflect:
        - Incoming: Yes
        - Outgoing: N/A
        - _Sent_ update: Yes
      - Delivery receipts: Yes
      - Reactions: Yes
      - When rejected: N/A (deprecated message is not being sent)
      - Edit applies to: N/A
      - Deletable by: User and sender
      - Send to Threema Gateway ID group creator: N/A

      **Properties (Group)**:
      - Kind: Group
      - Flags:
        - `0x01`: Send push notification.
      - User profile distribution: Yes
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: No
      - Protect against replay: Yes
      - Unarchive: Yes
      - Bump _last update_: Yes
      - Reflect:
        - Incoming: Yes
        - Outgoing: N/A
        - _Sent_ update: Yes
      - Delivery receipts: N/A
      - Reactions: Yes
      - When rejected: N/A (deprecated message is not being sent)
      - Edit applies to: N/A
      - Deletable by: User and sender
      - Send to Threema Gateway ID group creator: If capture is enabled

      The video is uploaded to the blob server and encrypted by:

          XSalsa20-Poly1305(key=<deprecated-video.key>, nonce=00..01)

      The thumbnail must be in JPEG format, is uploaded to the blob server
      and encrypted by:

          XSalsa20-Poly1305(key=<deprecated-video.key>, nonce=00..02)

      This message is deprecated and may no longer be submitted.

      When reflected from another device as an incoming 1:1 message:

      1. Run the _Common Deprecated Audio Receive Steps_.

      When receiving this message as a 1:1 message:

      1. Run the _Common Deprecated Audio Receive Steps_.

      When reflected from another device as an incoming group message (wrapped
      by [`group-member-container`](ref:e2e.group-member-container)):

      1. Run the _Common Deprecated Audio Receive Steps_.

      When receiving this message as a group message (wrapped by
      [`group-member-container`](ref:e2e.group-member-container)):

      1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the
         message has been discarded, abort these steps.
      2. Run the _Common Deprecated Audio Receive Steps_.

      The following steps are defined as the _Common Deprecated Video Receive
      Steps_:

      1. Add the message to the associated conversation.
      2. If this message is eligible for auto-download, schedule downloading the
         video data from the blob server. Only request the blob to be removed if
         the associated conversation is a 1:1 conversation.
    fields:
      - _doc: |-
          Video duration in seconds.
        name: duration
        type: u16-le
      - _doc: |-
          Blob ID to obtain the video data.
        name: video-blob-id
        type: *blob-id
      - _doc: |-
          Video size in bytes.
        name: video-size
        type: u32-le
      - _doc: |-
          Blob ID to obtain the thumbnail in JPEG format.
        name: thumbnail-blob-id
        type: *blob-id
      - _doc: |-
          Thumbnail size in bytes.
        name: thumbnail-size
        type: u32-le
      - _doc: |-
          Random symmetric key used to encrypt the video and thumbnail data.
        name: key
        type: *key

  file:
    _group: Conversation Messages
    _doc: |-
      A file or media message.

      **Properties (1:1)**:
      - Kind: 1:1
      - Flags:
        - `0x01`: Send push notification.
      - User profile distribution: Yes
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: Yes
      - Protect against replay: Yes
      - Unarchive: Yes
      - Bump _last update_: Yes
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes
        - _Sent_ update: Yes
      - Delivery receipts: Yes
      - Reactions: Yes
      - When rejected: Re-send after confirmation
      - Edit applies to: Caption
      - Deletable by: User and sender
      - Send to Threema Gateway ID group creator: N/A

      **Properties (Group)**:
      - Kind: Group
      - Flags:
        - `0x01`: Send push notification.
      - User profile distribution: Yes
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: No
      - Protect against replay: Yes
      - Unarchive: Yes
      - Bump _last update_: Yes
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes
        - _Sent_ update: Yes
      - Delivery receipts: N/A
      - Reactions: Yes
      - When rejected: Re-send after confirmation
      - Edit applies to: Caption
      - Deletable by: User and sender
      - Send to Threema Gateway ID group creator: If capture is enabled

      The file is uploaded to the blob server and encrypted by:

          XSalsa20-Poly1305(key=<file.key>, nonce=00..01)

      The thumbnail is uploaded to the blob server and encrypted by:

          XSalsa20-Poly1305(key=<file.key>, nonce=00..02)

      When the user submits this message in a 1:1, group or distribution list
      conversation:

      1. Let `file` be all necessary properties to construct this message.
      2. If the UTF-8 encoded bytes of `file.name` exceed 256 bytes, discard
         `file`, log an error and abort these steps.¹
      3. If the UTF-8 encoded bytes of `file.caption` exceed 6000 bytes, discard
         `file`, log an error and abort these steps.¹
      4. Run the _Messages Submit Steps_ with `messages` set from `file` (for a
         group, wrapped by
         [`group-member-container`](ref:e2e.group-member-container)).

      ¹: The UI should prevent submission in these cases.

      When reflected from another device as an incoming or outgoing 1:1
      message:

      1. Run the _Common File Receive Steps_.

      When receiving this message as a 1:1 message:

      1. Run the _Common File Receive Steps_.

      When reflected from another device as an incoming or outgoing group
      message (wrapped by
      [`group-member-container`](ref:e2e.group-member-container)):

      1. Run the _Common File Receive Steps_.

      When receiving this message as a group message (wrapped by
      [`group-member-container`](ref:e2e.group-member-container)):

      1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the
         message has been discarded, abort these steps.
      2. Run the _Common File Receive Steps_.

      The following steps are defined as the _Common File Receive Steps_:

      1. Add the message to the associated conversation.
      2. Schedule auto-downloading the thumbnail data from the blob server. Only
         request the blob to be removed if the associated conversation is a 1:1
         conversation.
      3. If this message is eligible for auto-download, schedule downloading the
         file data from the blob server. Only request the blob to be removed if
         the associated conversation is a 1:1 conversation.
    fields:
      - _doc: |-
          UTF-8, JSON-encoded object with the following fields:

          - Rendering type (`'j'`):
            - `0`: Render as a file.
            - `1`: Render as media (e.g. an image, audio or video).
            - `2`: Render as a sticker (for transparent images).

            If this field is not set, fall back to the value of `'i'`. If no
            value could be determined or the rendering type is unassigned,
            assume `0`.
          - Deprecated (`'i'`): Set this to the integer `1` if the rendering
            type is `1` or `2`, otherwise set this to the integer `0`.
          - Encryption key (`'k'`): Random symmetric key used to encrypt the
            blobs (file and thumbnail data) in lowercase hex string.
          - File blob ID (`'b'`): Blob ID in lowercase hex string
            representation to obtain the file data.
          - File media type (`'m'`): The media type of the file.
          - File name (`'n'`): Optional filename of the file.
          - File size (`'s'`): File size in bytes.
          - Thumbnail Blob ID (`'t'`): Optional blob containing the thumbnail
            file data.
          - Thumbnail media type (`'p'`): Media type of the thumbnail.
            If not set, assume `image/jpeg`.
          - Caption (`'d'`): Optional caption text.
          - Correlation ID (`'c'`): Optional random 32 byte ASCII string to
            collocate multiple media files.
          - Metadata (`'x'`): An optional metadata object as defined below.

          Metadata object fields depend on the media type of the file. All
          fields are optional but recommended to set in order to determine the
          layout logic while the file is being downloaded. Once the file has
          been parsed, the parsed data supersedes this object.

          For images:

          - Width (`'w'`): The width as an integer in px.
          - Height (`'h'`): The height as an integer in px.
          - Animated (`'a'`): Set this to the boolean `true` if the image is
            animated (e.g. an animated GIF).

          For audio:

          - Duration (`'d'`): The duration as a float in seconds.

          For video:

          - Width (`'w'`): The width as an integer in px.
          - Height (`'h'`): The height as an integer in px.
          - Duration (`'d'`): The duration as a float in seconds.

          Note that the rendering logic depends on three key fields which
          should be set accordingly:

          - Media type,
          - Rendering type,
          - Animated flag in the metadata object.
        name: file
        type: b*

  poll-setup:
    _group: Conversation Messages
    _doc: |-
      Creates a new poll or finalises an existing poll.

      During the lifecycle of a poll, this message will be used exactly twice:
      Once to create the poll, and once to close it.

      **Properties (1:1)**:
      - Kind: 1:1
      - Flags:
        - `0x01`: Send push notification.
      - User profile distribution: Yes
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: Yes
      - Protect against replay: Yes
      - Unarchive: Yes
      - Bump _last update_: Yes
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes
        - _Sent_ update: Yes
      - Delivery receipts: Yes
      - Reactions: Yes
      - When rejected: Re-send after confirmation
      - Edit applies to: N/A
      - Deletable by: User only (TODO(SE-383))
      - Send to Threema Gateway ID group creator: N/A

      **Properties (Group)**:
      - Kind: Group
      - Flags:
        - `0x01`: Send push notification.
      - User profile distribution: Yes
      - Exempt from blocking: Yes
      - Implicit _direct_ contact creation: No
      - Protect against replay: Yes
      - Unarchive: Yes
      - Bump _last update_: Yes
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes
        - _Sent_ update: Yes
      - Delivery receipts: N/A
      - Reactions: Yes
      - When rejected: Re-send after confirmation
      - Edit applies to: N/A
      - Deletable by: User only (TODO(SE-383))
      - Send to Threema Gateway ID group creator: If capture is enabled

      When the user submits this message in a 1:1 or group conversation:

      1. Let `poll` be all necessary properties to construct this message.
      2. If the UTF-8 encoded bytes of `poll.description` exceed 256 bytes,
         discard `poll`, log an error and abort these steps.¹
      3. If the UTF-8 encoded bytes of any of the `poll.choices` exceeds 256
         bytes, discard `poll`, log an error and abort these steps.¹
      4. JSON encode `poll` and let `encoded-poll` be the UTF-8 encoded result.
      5. If `encoded-poll` exceeds 6000 bytes, discard `poll` (and
         `encoded-poll`), log an error and abort these steps.¹
      6. Run the _Messages Submit Steps_ with `messages` set from
         `encoded-poll` (for a group, wrapped by
         [`group-member-container`](ref:e2e.group-member-container)).

      ¹: The UI should prevent submission in these cases.

      When reflected from another device as an incoming or outgoing 1:1
      message:

      1. Run the _Common Poll Setup Receive Steps_.

      When receiving this message as a 1:1 message:

      1. Run the _Common Poll Setup Receive Steps_.

      When reflected from another device as an incoming or outgoing group
      message (wrapped by
      [`group-member-container`](ref:e2e.group-member-container)):

      1. Run the _Common Poll Setup Receive Steps_.

      When receiving this message as a group message (wrapped by
      [`group-member-container`](ref:e2e.group-member-container)):

      1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the
         message has been discarded, abort these steps.
      2. Run the _Common Poll Setup Receive Steps_.

      The following steps are defined as the _Common Poll Setup Receive Steps_:

      1. Let `state` be the _State_ field of the message. Let `participants` be
         the _Participants_ field of the message.
      2. Look up the poll with the given ID within the conversation.
      3. If no associated poll could be found:
          1. If `state` is `1` (closed), discard the message and abort these steps.
          2. Add the poll to the associated conversation with the provided
             fields of the message and abort these steps.
      4. If the associated poll is closed, discard the message and abort these steps.
      5. If `state` is `0` (open), discard the message and abort these steps.
      6. Close the poll with the given `participants`, ignore any other fields
         of the message.
    fields:
      - _doc: |-
          Random unique (per creator within this conversation) ID of the poll.
        name: id
        type: *poll-id
      - _doc: |-
          UTF-8, JSON-encoded object with the following fields:

          - Description (`'d'`): A short description/topic string for the poll.
          - State (`'s'`):
            - `0`: Poll is _open_ for votes.
            - `1`: Poll has been _closed_.

            A state transition from _closed_ to _open_ is illegal and must be
            ignored by the receiving client.
          - Answer type (`'a'`):
            - `0`: Single choice poll.
            - `1`: Multiple choice poll.

            Any transition from one of the types to another is illegal and
            must be ignored by the receiving client.
          - Announce type (`'t'`):
            - `0`: Announce votes in form of the `poll-vote` message
              only to the creator of the ballot. Results are invisible until
              the creator closes the vote and reports the final results.
            - `1`: Announce votes in form of the `poll-vote` message to
              everyone in the conversation. Interim results are therefore
              visible to everyone.

            Any transition from one of the types to another is illegal and
            must be ignored by the receiving client.
          - Display mode (`'u'`):
            - `0`: List mode. List choices of all participants as presented
              by this message.
            - `1`: Summary mode. Only display the total amount of votes per
              choice and the user's vote (if any).

            If the field is not present, assume _list_ mode (`0`). Any
            transition from one of the modes to another is illegal and must be
            ignored by the receiving client.
          - Choices type (`'o'`, DEPRECATED): Always set this to the integer `0`.
          - Participants (`'p'`): A list of Threema IDs that participated in
            the poll (i.e. they cast a vote). This field must only be
            present if the poll is being _closed_. In display mode _summary_,
            this field should be an empty list and must be ignored by the
            receiver.
          - Choices (`'c'`): A list of choice objects as defined below.

          Choice object fields:

          - Choice ID (`'i'`): A per-poll unique ID of the choice in form of
            an integer. Used when casting a vote.
          - Description (`'n'`): Choice description in form of a string.
          - Sort key (`'o'`, DEPRECATED): Set this to the index of the choice
            object within the _choices_ list.
          - Participant votes (`'r'`): A list of indices referring to the
            index of the participant (as defined in the _participants_ list)
            that cast a vote for this choice. This field must only be present
            if the poll is being _closed_. In display mode _summary_ this
            should be an empty list and must be ignored by the receiver.
          - Total amount of votes (`'t'`): The total amount of votes for this
            choice. This field must only be present if the poll is being
            _closed_. In display mode _normal_ this field should not be
            present and must be ignored by the receiver.
        name: poll
        type: b*

  poll-vote:
    _group: Conversation Messages
    _doc: |-
      Cast a vote on a poll.

      **Properties (1:1)**:
      - Kind: 1:1
      - Flags: None
      - User profile distribution: Yes
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: No
      - Protect against replay: Yes
      - Unarchive: No¹
      - Bump _last update_: No¹
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes
        - _Sent_ update: No
      - Delivery receipts: No
      - Reactions: No
      - When rejected: N/A (ignored)
      - Edit applies to: N/A (can just send another `poll-vote`)
      - Deletable by: User only (TODO(SE-383))
      - Send to Threema Gateway ID group creator: N/A

      **Properties (Group)**:
      - Kind: Group
      - Flags: None
      - User profile distribution: Yes
      - Exempt from blocking: Yes
      - Implicit _direct_ contact creation: No
      - Protect against replay: Yes
      - Unarchive: No¹
      - Bump _last update_: No¹
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes
        - _Sent_ update: No
      - Delivery receipts: N/A
      - Reactions: No
      - When rejected: N/A (ignored)
      - Edit applies to: N/A (can just send another `poll-vote`)
      - Deletable by: User only (TODO(SE-383))
      - Send to Threema Gateway ID group creator: If capture is enabled

      ¹: A [`poll-vote`](ref:e2e.poll-vote) updates the poll state initiated by
      a corresponding [`poll-setup`](ref:e2e.poll-setup), meaning it does not
      produce a new visible message.

      When the user submits this message in a 1:1 or group conversation by
      casting a vote:

      1. Let `vote` be all necessary properties to construct this message.
      2. JSON encode `vote` and let `encoded-vote` be the UTF-8 encoded result.
      3. If `encoded-vote` exceeds 6000 bytes, discard `vote` (and
         `encoded-vote`), log an error and abort these steps.¹
      4. Run the _Messages Submit Steps_ with `messages` set from
         `encoded-vote` (for a group, wrapped by
         [`group-member-container`](ref:e2e.group-member-container)).

      ¹: The UI should prevent submission in these cases.

      When reflected from another device as an incoming or outgoing 1:1
      message:

      1. Run the _Common Poll Vote Receive Steps_.

      When receiving this message as a 1:1 message:

      1. Run the _Common Poll Vote Receive Steps_.

      When reflected from another device as an incoming or outgoing group
      message (wrapped by
      [`group-member-container`](ref:e2e.group-member-container)):

      1. Run the _Common Poll Vote Receive Steps_.

      When receiving this message as a group message (wrapped by
      [`group-member-container`](ref:e2e.group-member-container)):

      1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the
         message has been discarded, abort these steps.
      2. Run the _Common Poll Vote Receive Steps_.

      The following steps are defined as the _Common Poll Vote Receive Steps_.

      1. Look up the poll with the given ID within the conversation.
      2. If no associated poll could be found or if the associated poll is
         closed, discard the message and abort these steps.
      3. Update the poll with the provided choices of the sender.
    fields:
      - _doc: |-
          ID of the associated poll.
        name: poll-id
        type: *poll-id
      - _doc: |-
          The Threema ID of the creator of the poll.
        name: creator-identity
        type: *identity
      - _doc: |-
          UTF-8, JSON-encoded list containing one or more choice tuples. Each
          choice tuple contains the following two integer values:

          - Choice ID, referring to the Choice ID defined in the
            `poll-setup` message.
          - Selected:
            - `0`: The choice has not been selected.
            - `1`: The choice has been selected.

          Note: For protocol simplicity, a vote must always include all possible
          choices, whether or not they have been selected.
        name: choices
        type: b*

  call-offer:
    _group: Conversation Messages
    _doc: |-
      Initiates a call.

      **Properties**:
      - Kind: 1:1
      - Flags:
        - `0x01`: Send push notification.
        - `0x20`: Short-lived server queuing.
      - User profile distribution: Yes
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: Yes
      - Protect against replay: Yes
      - Unarchive: Yes
      - Bump _last update_: Yes
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes
        - _Sent_ update: No
      - Delivery receipts: No
      - Reactions: No
      - When rejected: Abort call
      - Edit applies to: N/A
      - Deletable by: User only (TODO(SE-384))
      - Send to Threema Gateway ID group creator: N/A

      [//]: # "When submit / receiving / reflected: TODO(SE-102)"
    fields:
      - _doc: |-
          UTF-8, JSON-encoded object with the following fields:

          - Call ID (`'callId'`): Random 32 bit unsigned integer greater than 0
            that uniquely identifies a call throughout its lifetime. Assume
            `0` if not set.
          - WebRTC Offer (`'offer'`): An offer object.
          - Feature negotiation (`'features'`): Optional Call Features object.

          Offer object fields:

          - WebRTC Offer SDP type (`'sdpType'`): Set this to `'offer'` and ignore
            offers with other types.
          - WebRTC Offer SDP (`'sdp'`): Opaque string containing the SDP.
        name: offer
        type: b*

  call-answer:
    _group: Conversation Messages
    _doc: |-
      Answer or reject a call.

      **Properties**:
      - Kind: 1:1
      - Flags:
        - `0x01`: Send push notification.
        - `0x20`: Short-lived server queuing.
      - User profile distribution: Only if accepted (`action`: `1`)
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: No
      - Protect against replay: Yes
      - Unarchive: No¹
      - Bump _last update_: No¹
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes
        - _Sent_ update: No
      - Delivery receipts: No
      - Reactions: No
      - When rejected: Abort call
      - Edit applies to: N/A
      - Deletable by: User only (TODO(SE-384))
      - Send to Threema Gateway ID group creator: N/A

      ¹: A [`call-answer`](ref:e2e.call-answer) updates the call state initiated
      by a corresponding [`call-offer`](ref:e2e.call-offer), meaning it does not
      produce a new visible message.

      [//]: # "When submit / receiving / reflected: TODO(SE-102)"
    fields:
      - _doc: |-
          UTF-8, JSON-encoded object with the following fields:

          - Call ID (`'callId'`): Random 32 bit unsigned integer greater than 0
            that uniquely identifies a call throughout its lifetime. Assume
            `0` if not set.
          - Required action (`'action'`):
            - `0`: The call has been rejected and needs to be aborted.
            - `1`: The call has been accepted and a connection needs to be
              established.
          - Rejection reason (`'rejectReason'`): If the call has been rejected,
            this field contains a reject reason:
            - `0`: Generic or unspecified rejection.
            - `1`: The callee is busy (another call is active).
            - `2`: The callee did not accept the call in time.
            - `3`: The callee explicitly rejected the call.
            - `4`: The callee disabled calls.
            - `5`: The callee was called during an off-hour period.
          - WebRTC Answer (`'answer'`): An answer object.
          - Feature negotiation (`'features'`): Optional Call Features object.

          Answer object fields:

          - WebRTC Answer SDP type (`'sdpType'`): Set this to `'answer'` and ignore
            answers with other types.
          - WebRTC Answer SDP (`'sdp'`): Opaque string containing the SDP.
        name: answer
        type: b*

  call-ice-candidate:
    _group: Conversation Messages
    _doc: |-
      An ICE candidate for an ongoing call.

      **Properties**:
      - Kind: 1:1
      - Flags:
        - `0x01`: Send push notification.
        - `0x20`: Short-lived server queuing.
      - User profile distribution: No
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: No
      - Protect against replay: No¹
      - Unarchive: No
      - Bump _last update_: No
      - Reflect:
        - Incoming: Yes
        - Outgoing: No
        - _Sent_ update: No
      - Delivery receipts: No
      - Reactions: No
      - When rejected: N/A (ignored)
      - Edit applies to: N/A
      - Deletable by: N/A
      - Send to Threema Gateway ID group creator: N/A

      ¹: This message does not trigger any kind of reaction and adding ICE
      candidates again has no ill-effect.

      [//]: # "When submit / receiving / reflected: TODO(SE-102)"
    fields:
      - _doc: |-
          UTF-8, JSON-encoded object with the following fields:

          - Call ID (`'callId'`): Random 32 bit unsigned integer greater than 0
            that uniquely identifies a call throughout its lifetime. Assume
            `0` if not set.
          - Deprecated (`'removed'`): Always set this to `false` and ignore
            messages with this field set to `true`.
          - WebRTC Candidates (`'candidates'`): An array of candidate objects.

          Candidate object fields:
          - WebRTC Candidate SDP (`'candidate'`): Opaque string containing the
            ICE candidate SDP.
          - WebRTC MID (`'sdpMid'`): Media stream identification string or
            `null`.
          - WebRTC Media Line Index (`'sdpMLineIndex'`): Media description
            line index integer or `null`.
          - WebRTC Username Fragment (`'ufrag'`): ICE username fragment or
            `null`.
        name: candidates
        type: b*

  call-hangup:
    _group: Conversation Messages
    _doc: |-
      Hang up a call.

      **Properties**:
      - Kind: 1:1
      - Flags:
        - `0x01`: Send push notification.
      - User profile distribution: No
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: No
      - Protect against replay: Yes
      - Unarchive: If no corresponding `call-offer` can be found¹
      - Bump _last update_: If no corresponding `call-offer` can be found¹
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes
        - _Sent_ update: No
      - Delivery receipts: No
      - Reactions: No
      - When rejected: N/A (ignored)
      - Edit applies to: N/A
      - Deletable by: User only (TODO(SE-384))
      - Send to Threema Gateway ID group creator: N/A

      ¹: A [`call-hangup`](ref:e2e.call-hangup) usually updates the call state
      initiated by a corresponding [`call-offer`](ref:e2e.call-offer), meaning
      it does not produce a new visible message. However, the
      [`call-offer`](ref:e2e.call-offer) uses the _Short-lived server queuing_
      flag and therefore may be lost. In that case, the
      [`call-hangup`](ref:e2e.call-hangup) does create a visible _call missed_
      message.

      [//]: # "When submit / receiving / reflected: TODO(SE-102)"
    fields:
      - _doc: |-
          UTF-8, JSON-encoded object. If this field contains zero bytes, assume
          an empty object. Contains the following fields:

          - Call ID (`'callId'`): Random 32 bit unsigned integer greater than 0
            that uniquely identifies a call throughout its lifetime. Assume
            `0` if not set.
        name: hangup
        type: b*

  call-ringing:
    _group: Conversation Messages
    _doc: |-
      Sent by the callee to indicate that the call is ringing.

      **Properties**:
      - Kind: 1:1
      - Flags:
        - `0x01`: Send push notification.
        - `0x20`: Short-lived server queuing.
      - User profile distribution: No
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: No
      - Protect against replay: Yes
      - Unarchive: No¹
      - Bump _last update_: No¹
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes
        - _Sent_ update: No
      - Delivery receipts: No
      - Reactions: No
      - When rejected: Abort call
      - Edit applies to: N/A
      - Deletable by: N/A
      - Send to Threema Gateway ID group creator: N/A

      ¹: A [`call-ringing`](ref:e2e.call-ringing) updates the call state
      initiated by a corresponding [`call-offer`](ref:e2e.call-offer), meaning
      it does not produce a new visible message.

      [//]: # "When submit / receiving / reflected: TODO(SE-102)"
    fields:
      - _doc: |-
          UTF-8, JSON-encoded object. If this field contains zero bytes, assume
          an empty object. Contains the following fields:

          - Call ID (`'callId'`): Random 32 bit unsigned integer greater than 0
            that uniquely identifies a call throughout its lifetime. Assume
            `0` if not set.
        name: hangup
        type: b*

  delivery-receipt:
    _group: Status Updates
    _doc: |-
      Confirms reception or delivers detailed status updates of a message.

      **Properties (1:1)**:
      - Kind: 1:1
      - Flags: None
      - User profile distribution: Only for reactions
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: No
      - Protect against replay: Only for reactions¹
      - Unarchive: No
      - Bump _last update_: No
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes²
        - _Sent_ update: No
      - Delivery receipts: No, that would be silly!
      - Reactions: No (also silly)
      - When rejected: N/A (ignored)
      - Edit applies to: N/A (can just send another `delivery-receipt`)
      - Deletable by: N/A
      - Send to Threema Gateway ID group creator: N/A

      ¹: Repeating a status of type _received_ or _read_ has no ill-effects.

      ²: When the message is being _read_ and _read_ receipts are disabled, an
      `d2d.IncomingMessageUpdate` will be reflected instead.

      **Properties (Group)**:
      - Kind: Group
      - Flags: None
      - User profile distribution: Only for reactions
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: No
      - Protect against replay: Only for reactions¹
      - Unarchive: No
      - Bump _last update_: No
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes. When the message is being _read_ and _read_ receipts
          are disabled, reflect an `d2d.IncomingMessageUpdate` (since no
          `delivery-receipt` is sent in this case).
        - _Sent_ update: No
      - Delivery receipts: No, that would be silly!
      - Reactions: No (also silly)
      - When rejected: N/A (ignored)
      - Edit applies to: N/A (can just send another `delivery-receipt`)
      - Deletable by: N/A
      - Send to Threema Gateway ID group creator: If capture is enabled

      ¹: Repeating a status of type _received_ or _read_ has no ill-effects.

      When the user opens a 1:1 conversation or marks it as _read_ by another
      mechanism:

      1. Let `message-ids` be all message IDs associated to messages of the
         conversation that require _automatic_ delivery receipts and which are
         not yet marked as _read_.
      2. Split the provided `message-ids` into bundles of at most 875 message
         IDs and let `message-ids-bundles` be the result.¹
      3. Run the _Messages Submit Steps_ with `messages` set to create one or
         more [`delivery-receipt`](ref:e2e.delivery-receipt)s from
         `message-ids-bundles` with status `0x02`.

      ¹: Each message ID has 8 bytes, divided by at most 7000 bytes.

      When reflected from another device as an incoming 1:1 message:

      1. Run the _Common Incoming Delivery Receipt Steps_.

      When reflected from another device as an outgoing 1:1 message:

      1. Run the _Common Outgoing Delivery Receipt Steps_.

      When receiving this message as a 1:1 message:

      1. Run the _Common Incoming Delivery Receipt Steps_.

      When reflected from another device as an incoming group message (wrapped
      by [`group-member-container`](ref:e2e.group-member-container)):

      1. If `status` is not `0x03` or `0x04` (i.e. not a reaction), log a
         notice, discard the message and abort these steps.
      2. Run the _Common Incoming Delivery Receipt Steps_.

      When reflected from another device as an outgoing group message (wrapped
      by [`group-member-container`](ref:e2e.group-member-container)):

      1. If `status` is not `0x03` or `0x04` (i.e. not a reaction), log a
         notice, discard the message and abort these steps.
      2. Run the _Common Outgoing Delivery Receipt Steps_.

      When receiving this message as a group message (wrapped by
      [`group-member-container`](ref:e2e.group-member-container)):

      1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the
         message has been discarded, abort these steps.
      2. If `status` is not `0x03` or `0x04` (i.e. not a reaction), log a
         notice, discard the message and abort these steps.
      3. Run the _Common Incoming Delivery Receipt Steps_.

      The following steps are defined as the _Common Incoming Delivery Receipt
      Steps_:

      1. For each `message-id` of `message-ids`:
         1. Lookup the associated message for `message-id` in the associated
            conversation and let `referred-message` be the result.
         2. If `referred-message` is not defined, abort these sub-steps.
         3. If the associated conversation is a 1:1 conversation and the
            original sender of `referred-message` is not the user, abort these
            sub-steps.
         4. If `status` is `0x01` or `0x02` (i.e. a delivery receipt) and
            `referred-message` allows for delivery receipts (see the associated
            _Delivery receipts_ property), apply and replace the delivery
            receipt of the sender to `referred-message` with the associated
            timestamp set to the message's (the `delivery-receipt`'s)
            `created-at`.
         5. If `status` is `0x03` or `0x04` (i.e. a reaction) and
            `referred-message` is reactable (see the associated _Reactions_
            property), apply and replace any existing reaction of the sender to
            `referred-message` with the reaction timestamp set to the message's
            (the `delivery-receipt`'s) `created-at`.¹

      The following steps are defined as the _Common Outgoing Delivery Receipt
      Steps_:

      1. For each `message-id` of `message-ids`:
         1. Lookup the associated message for `message-id` in the associated
            conversation and let `referred-message` be the result.
         2. If `referred-message` is not defined, abort these sub-steps.
         3. If the associated conversation is a 1:1 conversation and the
            original sender of `referred-message` is the user, abort these
            sub-steps.
         4. If `status` is `0x01` or `0x02` (i.e. a delivery receipt) and
            `referred-message` allows for delivery receipts (see the associated
            _Delivery receipts_ property), apply and replace the delivery
            receipt of the user to `referred-message` with the associated
            timestamp set to the message's (the `delivery-receipt`'s)
            `created-at`.
         5. If `status` is `0x03` or `0x04` (i.e. a reaction) and
            `referred-message` is reactable (see the associated _Reactions_
            property), apply and replace any existing reaction of the user to
            `referred-message` with the reaction timestamp set to the message's
            (the `delivery-receipt`'s) `created-at`.¹

      ¹: Note that the deprecated reactions transmitted by a `delivery-receipt`
      always replace **all existing reactions** of the respective sender,
      including new-style reactions transmitted by a `csp_e2e.Reaction` message.
    fields:
      - _doc: |-
          Message status:

          - `0x01`: Message was received.
          - `0x02`: Message was read.
          - `0x03`: **Deprecated** Maps to the 👍 emoji (`1F44D`).
          - `0x04`: **Deprecated** Maps to the 👎 emoji (`1F44E`).

          Note that only the `0x01` and `0x02` variants are considered true
          _delivery receipts_ whereas the deprecated `0x03` and `0x04` variants
          are considered _reactions_ (just like `csp_e2e.Reaction`).

          The following replacement logic is to be applied on a message's
          status when displayed:

          1. `0x02` replaces groups listed below,
          2. `0x01` replaces the unlisted _created_ status.
        name: status
        type: u8
      - _doc: |-
          One or more `message-id`s whose status should be updated.
        name: message-ids
        type: *message-ids

  typing-indicator:
    _group: Status Updates
    _doc: |-
      Indicates whether a contact is currently typing.

      **Properties**:
      - Kind: 1:1
      - Flags:
        - `0x02`: No server queuing.
        - `0x04`: No server acknowledgement.
      - User profile distribution: No
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: No
      - Protect against replay: No¹
      - Unarchive: No
      - Bump _last update_: No
      - Reflect:
        - Incoming: Yes
        - Outgoing: No
        - _Sent_ update: No
      - Delivery receipts: No
      - Reactions: No
      - When rejected: N/A (ignored)
      - Edit applies to: N/A
      - Deletable by: N/A
      - Send to Threema Gateway ID group creator: N/A

      ¹: It is deemed acceptable if the _typing_ indicator in the UI is replayed
      since there is no further consequence.

      When the user is currently _typing_ while composing a **new**¹ message in
      an associated conversation:

      1. Schedule a volatile task to run the _Bundled Messages Send Steps_ with
         the following properties:
         - `id` set to a random message ID,
         - `created-at` set to the current timestamp,
         - `receivers` set to the targeted receiver,
         - to construct this message with `is-typing` set to `1`.
      2. Start a _user is typing_ timer in the conversation to rerun these
         steps in 10s.

      When the user stopped _typing_ while composing a message in an associated
      conversation, or when the user left the conversation view:

      1. If no _user is typing_ timer is running for the conversation, abort
         these steps.
      2. Stop the _user is typing_ timer of the conversation.
      3. Schedule a volatile task to run the _Bundled Messages Send Steps_ with
         the following properties:
         - `id` set to a random message ID,
         - `created-at` set to the current timestamp,
         - `receivers` set to the targeted receiver,
         - to construct this message with `is-typing` set to `0`.

      When reflected from another device as an incoming message:

      1. Run the _Common Typing Indicator Receive Steps_.

      When receiving this message:

      1. Run the _Common Typing Indicator Receive Steps_.

      The following steps are defined as the _Common Typing Indicator Receive
      Steps_:

      1. If `is-typing` is `1`, start a timer to display that the sender is
         typing in the associated conversation for the next 15s.
      2. If `is-typing` is `0`, cancel any running timer displaying that the
         sender is typing in the associated conversation.

      ¹: Editing a message may not trigger _typing_.
    fields:
      - _doc: |-
          Set to `1` in case the contact is currently typing or `0` in
          case the contact stopped typing. Other values are invalid.
        name: is-typing
        type: u8

  set-profile-picture:
    _group: Contact and Group Control
    _doc: |-
      Set the profile picture of a contact or a group.

      **Properties (1:1)**:
      - Kind: 1:1
      - Flags: None
      - User profile distribution: No (obviously)
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: No
      - Protect against replay: Yes
      - Unarchive: No
      - Bump _last update_: No
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes
        - _Sent_ update: No
      - Delivery receipts: No
      - Reactions: No
      - When rejected: N/A (ignored)
      - Edit applies to: N/A (can just send another `set-profile-picture`)
      - Deletable by: N/A (can just send a `delete-profile-picture`)
      - Send to Threema Gateway ID group creator: N/A

      **Properties (Group)**:
      - Kind: Group
      - Flags: None
      - User profile distribution: No (obviously)
      - Exempt from blocking: Yes
      - Implicit _direct_ contact creation: No
      - Protect against replay: Yes
      - Unarchive: No
      - Bump _last update_: No
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes
        - _Sent_ update: No
      - Delivery receipts: N/A
      - Reactions: No
      - When rejected: N/A¹
      - Edit applies to: N/A (can just send another `set-profile-picture`)
      - Deletable by: N/A (can just send a `delete-profile-picture`)
      - Send to Threema Gateway ID group creator: N/A

      ¹: For the group creator it will be handled as if `group-sync-request` was
      received, re-sending the group profile picture state, implicitly triggered
      by FS `Reject` receive steps.

      The profile picture must be in JPEG format, is uploaded to the blob
      server and encrypted by:

          XSalsa20-Poly1305(key=<set-profile-picture.key>, nonce=00..01)

      When reflected from another device as an outgoing 1:1 message:

      1. Update the most recently distributed profile picture cache for the
         contact to the enclosed blob ID.

      When receiving this message as a 1:1 message:

      1. Download the picture from the blob server but do not request the blob
         to be removed. Store the profile picture.
      2. Store the picture as the _contact-defined_ profile picture and run the
         _Contact Profile Picture Selection Steps_.

      When receiving this message as a group message (wrapped by
      [`group-creator-container`](ref:e2e.group-creator-container)):

      1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the
         message has been discarded, abort these steps.
      2. Download the picture from the blob server but do not request the blob
         to be removed. Let `profile-picture` be the result.
      3. Let `group` be a snapshot of the current group state.
      4. If `group.profile-picture` is defined and equals `profile-picture`
         (i.e. no changes), discard the message and abort these steps.
      5. (MD) Run the following sub-steps:
         1. (MD) Begin a transaction with scope `GROUP_SYNC` and the following
            precondition:
            1. If the group does not exist or the group is marked as _left_, log
               a warning that a group sync race occurred, discard the message
               and abort these steps.
         2. (MD) Let `group` be a snapshot of the current group state.
         3. (MD) If `group.profile-picture` is defined and equals
            `profile-picture`, log a warning that a group sync race occurred.
         4. (MD) Reflect a `GroupSync.Update` with `group` set to contain
            `profile_picture` set to `profile-picture.
         5. (MD) Commit the transaction and await acknowledgement.
      6. Store the profile picture and and apply it to the group.
    fields:
      - _doc: |-
          Blob ID to obtain the image data.
        name: picture-blob-id
        type: *blob-id
      - _doc: |-
          Profile picture size in bytes.
        name: picture-size
        type: u32-le
      - _doc: |-
          Random symmetric key used to encrypt the image data.
        name: key
        type: *key

  delete-profile-picture:
    _group: Contact and Group Control
    _doc: |-
      Delete the profile picture of a contact.

      **Properties (1:1)**:
      - Kind: 1:1
      - Flags: None
      - User profile distribution: No (obviously)
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: No
      - Protect against replay: Yes
      - Unarchive: No
      - Bump _last update_: No
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes
        - _Sent_ update: No
      - Delivery receipts: No
      - Reactions: No
      - When rejected: N/A (ignored)
      - Edit applies to: N/A (can just send another `delete-profile-picture`)
      - Deletable by: N/A
      - Send to Threema Gateway ID group creator: N/A

      **Properties (Group)**:
      - Kind: Group
      - Flags: None
      - User profile distribution: No (obviously)
      - Exempt from blocking: Yes
      - Implicit _direct_ contact creation: No
      - Protect against replay: Yes
      - Unarchive: No
      - Bump _last update_: No
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes
        - _Sent_ update: No
      - Delivery receipts: N/A
      - Reactions: No
      - When rejected: N/A¹
      - Edit applies to: N/A (can just send another `delete-profile-picture`)
      - Deletable by: N/A
      - Send to Threema Gateway ID group creator: N/A

      ¹: For the group creator it will be handled as if `group-sync-request` was
      received, re-sending the group profile picture state, implicitly triggered
      by FS `Reject` receive steps.

      When reflected from another device as an incoming 1:1 message:

      1. Remove the _contact-defined_ profile picture and run the _Contact
         Profile Picture Selection Steps_.

      When reflected from another device as an outgoing 1:1 message:

      1. Update the most recently distributed profile picture cache for the
         contact to a _remove_ mark with the reflected timestamp.

      When receiving this message as a 1:1 message:

      1. Remove the _contact-defined_ profile picture and run the _Contact
         Profile Picture Selection Steps_.

      When receiving this message as a group message (wrapped by
      [`group-creator-container`](ref:e2e.group-creator-container)):

      1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the
         message has been discarded, abort these steps.
      2. Let `group` be a snapshot of the current group state.
      3. If `group.profile-picture` is not defined (i.e. no change), discard the
         message and abort these steps.
      4. (MD) Run the following sub-steps:
         1. Begin a transaction with scope `GROUP_SYNC` and the following
            precondition:
            1. If the group does not exist or the group is marked as _left_, log
               a warning that a group sync race occurred, discard the message
               and abort these steps.
         2. Let `group` be a snapshot of the current group state.
         3. If `group.profile-picture` is not defined, log a warning that a
            group sync race occurred.
         4. Reflect a `GroupSync.Update` with `group` set to contain
            `profile_picture` set to be removed.
         5. Commit the transaction and await acknowledgement.
      5. Remove the profile picture of the group.

  contact-request-profile-picture:
    _group: Contact and Group Control
    _doc: |-
      Request a contact's profile picture.

      Note that this message does not result in the profile picture being sent
      immediately in reply to this message. Instead, it will be sent the next
      time that contact sends a message to the user (if one is set, and if the
      user is eligible for receiving the profile picture).

      **Properties**:
      - Kind: 1:1
      - Flags: None
      - User profile distribution: No
      - Exempt from blocking: No
      - Implicit _direct_ contact creation: No
      - Protect against replay: Yes
      - Unarchive: No
      - Bump _last update_: No
      - Reflect:
        - Incoming: Yes
        - Outgoing: No
        - _Sent_ update: No
      - Delivery receipts: No
      - Reactions: No
      - When rejected: N/A (ignored)
      - Edit applies to: N/A
      - Deletable by: N/A
      - Send to Threema Gateway ID group creator: N/A

      When reflected from another device as an incoming message:

      1. Run the _Common Request Profile Picture Receive Steps_.

      When receiving this message:

      1. Run the _Common Request Profile Picture Receive Steps_.

      The following steps are defined as the _Common Request Profile Picture
      Receive Steps_:

      1. Purge the most recently distributed profile picture cache for the
         sender.

  group-setup:
    _group: Contact and Group Control
    _doc: |-
      Announces the group setup to all participants. The group creator is
      always a member of the group and must not be included in the member
      list.

      This is sent by the creator to create a new group, as well as update and
      disband an existing group. The group creator sends this message to all
      current (including those to be removed) and newly added group members.
      The group creator may also send this to a single receiver in special
      cases.

      Since the group creator is not allowed to leave the group, the only way
      for it to stop being a member is by sending a `group-setup` with an
      empty members list and thereby disbanding the group.

      **Properties**:
      - Kind: Group
      - Flags: None
      - User profile distribution: Yes
      - Exempt from blocking: Yes
      - Implicit _direct_ contact creation: Yes
      - Protect against replay: Yes
      - Unarchive: No¹
      - Bump _last update_: No²
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes
        - _Sent_ update: No
      - Delivery receipts: N/A
      - Reactions: No
      - When rejected: N/A³
      - Edit applies to: N/A (can just send another `group-setup`)
      - Deletable by: N/A (can just send another `group-setup`)
      - Send to Threema Gateway ID group creator: N/A

      ¹: A newly created group's conversation visibility is implicitly _normal_
      and therefore not _archived_. For the sake of simplicity and
      sender/receiver symmetry, further updates to the group should not alter
      the conversation visibility.

      ²: A newly created group is implicitly created with _last update_ set to
      the current timestamp. For the sake of simplicity and sender/receiver
      symmetry, no further updates to the group should bump _last update_.

      ³: For the group creator it will be handled as if `group-sync-request` was
      received, re-sending the group state, implicitly triggered by FS `Reject`
      receive steps.

      When receiving this message as a group message (wrapped by
      [`group-creator-container`](ref:e2e.group-creator-container)):

      1. Let `members` be the given member list. Remove all duplicate entries
         from `members`. Remove the sender (creator) from `members` if present.
      2. Look up the group.
      3. If the group could be found:
         1. Let `group` be a snapshot of the current group state.
         2. If the group is marked as _left_ and `members` is empty (i.e. no
            change), discard the message and abort these steps.
         3. If the group is not marked as _left_:
            1. Let `current-members` be a copy of `group.members`.
            2. Add the user to `current-members`.
            3. If `current-members` equals `members` (i.e. no change), discard
               the message and abort these steps.
      4. If `members` does not include the user:
         1. If the group could not be found, discard the message and abort these
            steps.
         2. (MD) Begin a transaction with scope `GROUP_SYNC` and the following
            precondition:
            1. If the group does not exist or the group is marked as _left_,
               discard the message and abort these steps.
         3. (MD) Reflect a `GroupSync.Update` with `group` set to contain the
            `user_state` set to `KICKED`.
         4. (MD) Commit the transaction and await acknowledgement.
         5. If the user is currently participating in a group call of this
            group, trigger leaving the call.
         6. Mark the group as _left_.
         7. Persist the previous member setup so that the group can be cloned.
         8. Run the _Rejected Messages Refresh Steps_ for the group.
      5. If `members` includes the user.
         1.  (MD) Begin a transaction with scope `GROUP_SYNC` and the following
             precondition:
             1. If the sender (creator) contact does not exist, log an error,
                discard the message and abort these steps.
         2.  Run the _Valid Contacts Lookup Steps_ for `members` and
             overwrite `members` with the result.
         3.  For each `contact-or-init` of `members`:
             1. If `contact-or-init` indicates that the _contact is the user_,
                remove the entry from `members` and abort these sub-steps.
             2. If `contact-or-init` indicates that the _contact is invalid_,
                remove the entry from `members`, log a warning and abort these
                sub-steps.
         4.  (MD) Let `pending-reflect-acks` be an empty list.
         5.  For each `contact-or-init` of `members`:
             1. If `contact-or-init` is an existing contact, abort these
                sub-steps.
             2. (MD) Reflect a `ContactSync.Create` with `contact` set from
                `contact-or-init` and the following additional properties:
                - `created_at` set to now,
                - `acquaintance_level` set to `GROUP`,
                - all policies and categories set to their defaults.
             3. (MD) Add the pending reflect acknowledgement to
                `pending-reflect-acks`.
         6.  (MD) Await all `pending-reflect-acks`.
         7.  Let `added-members` be a copy of `members`.
         8.  Let `group` be a snapshot of the current group state or undefined
             if the group does not exist.
         9.  If `group` is not defined:
             1. Let `removed-members` be an empty list.
             2. (MD) Reflect a `GroupSync.Create` with `group` set to contain:
                - `group_identity`,
                - `created_at`,
                - `name` empty,
                - `user_state` set to `MEMBER`,
                - `profile_picture` empty,
                - `member_identities` from `members`,
                - all policies and categories set to their defaults.
         10. If `group` is defined:
             1. Remove all members from `added-members` that are in
                `group.members`.
             2. Let `removed-members` be a copy of `group.members`.
             3. Remove all members from `removed-members` that are in `members`.
             4. (MD) Reflect a `GroupSync.Update` with `member_state_changes`
                constructed from `added-members` and `removed-members` and
                `group` set to contain the following additional properties:
                - `user_state` set to `MEMBER`,
                - `member_identities` from `members`.
         11. (MD) Commit the transaction and await acknowledgement.
         12. If the user is currently participating in a group call of this
             group, remove all `removed-members` participants from the group
             call (handle them as if they left the call) and unblock all pending
             group call flows for `added-members`.
         13. Persist newly added contacts from `members`.
         14. Persist the newly created group or the member changes to the group.
             If the group was previously marked as _left_, remove the _left_
             mark.
         15. TODO(SE-510): Schedule fetching gateway-defined profile picture
             here for each newly added contact from `members`, if necessary.
         16. If `added-members` or `removed-members` is not empty, run the
             _Rejected Messages Refresh Steps_ for the group.
    fields:
      - _doc: |-
          A set of Threema IDs defining group membership. The creator's
          Threema ID is always inferred and must not be included in this set.
        name: members
        type: *identities

  group-name:
    _group: Contact and Group Control
    _doc: |-
      Name (or rename) a group. Sent to all group members when the group is
      being created for the first time or the group is being renamed. May also
      be sent to a single receiver as a response to a
      [`group-sync-request`](ref:e2e.group-sync-request) message.

      **Properties**:
      - Kind: Group
      - Flags: None
      - User profile distribution: No
      - Exempt from blocking: Yes
      - Implicit _direct_ contact creation: No
      - Protect against replay: Yes
      - Unarchive: No
      - Bump _last update_: No
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes
        - _Sent_ update: No
      - Delivery receipts: N/A
      - Reactions: No
      - When rejected: N/A¹
      - Edit applies to: N/A (can just send another `group-name`)
      - Deletable by: N/A (can just send an empty name)
      - Send to Threema Gateway ID group creator: N/A

      ¹: For the group creator it will be handled as if `group-sync-request` was
      received, re-sending the group name, implicitly triggered by FS `Reject`
      receive steps.

      When receiving this message as a group message (wrapped by
      [`group-creator-container`](ref:e2e.group-creator-container)):

      1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the
         message has been discarded, abort these steps.
      2. Let `group` be a snapshot of the current group state.
      3. If `group.name` equals `name` (i.e. no change), discard the message and
         abort these steps.
      4. (MD) Run the following sub-steps:
         1. Begin a transaction with scope `GROUP_SYNC` and the following
            precondition:
            1. If the group does not exist or the group is marked as _left_, log
               a warning that a group sync race occurred, discard the message
               and abort these steps.
         2. Let `group` be a snapshot of the current group state.
         3. If `group.name` equals `name`, log a warning that a group sync race
            occurred.
         4. Reflect a `GroupSync.Update` with `group` set to contain
            `name` set to `name`.
         5. Commit the transaction and await acknowledgement.
      5. Update the group's name with `name`.
    fields:
      - _doc: |-
          UTF-8 encoded string containing the group's name.
        name: name
        type: b*

  group-leave:
    _group: Contact and Group Control
    _doc: |-
      Sent by a group member...

      * that is leaving the group. The message is sent to all other group
        members and the creator.
      * in direct reply to a group message for a group that it has marked as
        left.

      Note: The group creator is not allowed to leave the group.

      **Properties**:
      - Kind: Group
      - Flags: None
      - User profile distribution: No
      - Exempt from blocking: Yes
      - Implicit _direct_ contact creation: No
      - Protect against replay: Yes
      - Unarchive: No
      - Bump _last update_: No
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes
        - _Sent_ update: No
      - Delivery receipts: N/A
      - Reactions: No
      - When rejected: N/A¹
      - Edit applies to: N/A
      - Deletable by: N/A
      - Send to Threema Gateway ID group creator: Yes

      ¹: Re-send of `group-leave` implicitly triggered by FS `Reject` receive
      steps due to _Common Group Receive Steps_ invocation.

      When receiving this message as a group message (wrapped by
      [`group-member-container`](ref:e2e.group-member-container)):

      1. If the sender is the creator of the group, log a warning, discard the
         message and abort these steps.
      2. Look up the group.
      3. If the group could not be found or is marked as _left_:
          1. If the user is the creator of the group (as alleged by the
             message), discard the message and abort these steps.
          2. Run the _Group Sync Request Steps_ for the group, discard the
             message and abort these steps.
      4. Let `group` be a snapshot of the current group state.
      5. If `group.members` does not include the sender, discard the message and
         abort these steps.
      6. (MD) Run the following sub-steps:
         1. Begin a transaction with scope `GROUP_SYNC` and the following
            precondition:
            1. If the group does not exist or the group is marked as _left_, log
               a warning that a group sync race occurred, discard the message
               and abort these steps.
         2. Let `group` be a snapshot of the current group state.
         3. If `group.members` does not include the sender, log a warning that a
            group sync race occurred.
         4. Let `updated-members` be a copy of `group.members`.
         5. Remove the sender from `updated-members`.
         6. Reflect a `GroupSync.Update` with `member_state_changes`
            set to the single entry of the sender leaving and `group` set to
            contain `member_identities` set from `updated-members`.
         7. Commit the transaction and await acknowledgement.
      7. If the user is currently participating in a group call of this group,
         remove the sender from the group call (handle it as if the sender left
         the call).
      8. Remove the sender from the group.
      9. Run the _Rejected Messages Refresh Steps_ for the group.

  group-sync-request:
    _group: Contact and Group Control
    _doc: |-
      Sent by a group member (or a device assuming to be part of the group) to
      the group creator.

      **Properties**:
      - Kind: Group
      - Flags: None
      - User profile distribution: No
      - Exempt from blocking: Yes
      - Implicit _direct_ contact creation: No
      - Protect against replay: Yes
      - Unarchive: No
      - Bump _last update_: No
      - Reflect:
        - Incoming: Yes
        - Outgoing: Yes
        - _Sent_ update: No
      - Delivery receipts: N/A
      - Reactions: No
      - When rejected: N/A¹
      - Edit applies to: N/A
      - Deletable by: N/A
      - Send to Threema Gateway ID group creator: Yes

      ¹: Implicitly ignored by FS `Reject` receive steps.

      The following steps are defined as the _Group Sync Request Steps_:

      1. If the user is the creator of the group, log an error and abort these
         steps.
      2. If the group is marked as _recently resynced_ for the user, log a
         notice and abort these steps.¹
      3. Run the _Bundled Messages Send Steps_ with the following properties:
         - `id` set to a random message ID,
         - `created-at` set to the current timestamp,
         - `receivers` set to the creator of the group,
         - to construct this message (wrapped by
           [`group-member-container`](ref:e2e.group-member-container))
      4. Mark the group as _recently resynced_ for the user for 1h.

      When receiving this message as a group message (wrapped by
      [`group-member-container`](ref:e2e.group-member-container)):

      1. Look up the group. If the group could not be found, discard the message
         and abort these steps.
      2. If the user is not the creator of the group, discard the message and
         abort these steps.
      3. If the group is marked as _recently resynced_ for the sender, log a
         notice, discard the message and abort these steps.¹
      4. (MD) Begin a transaction with scope `GROUP_SYNC` and the following
         precondition:
         1. If the group does not exist, log a warning that a group sync race
            occurred, discard the message and abort these steps.
      5. If the group is marked as _left_ or the sender is not a member of the
         group, run the _Bundled Messages Send Steps_ with the following
         properties:
         - `id` set to a random message ID,
         - `created-at` set to the current timestamp,
         - `receivers` set to the sender,
         - to construct a [`group-setup`](ref:e2e.group-setup) (wrapped by
           [`group-creator-container`](ref:e2e.group-creator-container)) with an
           empty members set.
      6. If the group is not marked as _left_ and the sender is a member of the
         group, run the _Active Group State Resync Steps_ with four random message
         IDs and `target-members` set to the sender.
      7. (MD) Commit the transaction and await acknowledgement.

      ¹: This is a precaution since a `group-sync-request` is automatically
      triggered and creates an automatic response. This can easily lead to
      message loops. Limiting `group-sync-request`s to once an hour per group
      per sender/receiver breaks a potential infinite loop.

  web-session-resume:
    _group: Push Control
    _doc: |-
      A control message from Threema Web, requesting a session to be resumed.

      **Properties (1:1)**:
      - Kind: 1:1
      - Flags:
        - `0x20`: Short-lived server queuing.
      - User profile distribution: N/A (not sent by apps)
      - Exempt from blocking: Yes
      - Implicit _direct_ contact creation: N/A (blocking is circumvented)
      - Protect against replay: Yes
      - Unarchive: No
      - Bump _last update_: No
      - Reflect:
        - Incoming: No
        - Outgoing: No
        - _Sent_ update: No
      - Delivery receipts: N/A
      - Reactions: N/A
      - When rejected: N/A (not sent by clients)
      - Edit applies to: N/A
      - Deletable by: N/A
      - Send to Threema Gateway ID group creator: N/A

      When receiving this message:

      1. If the sender is not `*3MAPUSH`, discard the message and abort these
         steps.
      2. Lookup the web client session associated to `wcs` and attempt to resume
         it.
    fields:
      - _doc: |-
          UTF-8, JSON-encoded object with the following fields:

          - Webclient session (`'wcs'`): SHA256 hash (hex encoded) of the
            public permanent key of the session initiator, string.
          - Affiliation ID (`'wca'`): An optional identifier for affiliating
            consecutive pushes, `string` or `null`.
          - Affiliation ID (`'wct'`): Unix epoch timestamp of the request in
            seconds, `i64`.
          - Protocol version (`'wcv'`): Version of the Threema Web protocol,
            `u16`.

          All fields must be part of the JSON object, even if their values are
          nullable.
        name: push-payload
        type: b*

# Parsed struct namespaces (mapped into separate files)
namespaces:
  index: *index
  handshake: *handshake
  payload: *payload
  e2e: *e2e
