Blobs
Attachments are not transferred through the API endpoint. They are uploaded and downloaded over plain HTTP, at the URLs the session advertises, and the runtime handles both:
info, err := c.Upload(ctx, accountID, "application/pdf", file)
// info.BlobID now goes into an Email/set that attaches it.
blob, err := c.Download(ctx, accountID, part.BlobID, &jmapc.DownloadOptions{
Name: *part.Name,
Type: part.Type,
})
defer blob.Close()Both stream. Upload reads from an io.Reader and Download returns an
io.ReadCloser, so an attachment larger than memory is copied from a file to
the server, and from the server to a file, without being held in either
direction. An upload larger than the server’s maxSizeUpload fails before it
is sent.
From and Length fetch part of a blob, which is how a download interrupted
part way is resumed:
blob, err := c.Download(ctx, accountID, blobID, &jmapc.DownloadOptions{From: written})
...
blob.Range // the part that came back: 4096-8191 of 8192They are sent as an HTTP Range header. JMAP defines none for the download
endpoint, so a server is free to ignore it and answer with the whole blob.
HTTP does not let a server that answers with part of it answer with a part that
does not fit what was asked, but one that gets ranges wrong may: a part that
starts somewhere else, ends past where it was asked to, ends sooner while the
blob goes on, is not as long as it says, or does not say where it lies. Either way the download fails rather than returning content the
caller would write at the wrong offset, or handing the caller less than it
asked for. A part that ends sooner because the blob ends first is returned, and
blob.Range says how much came back. IsRangeIgnored reports the failure,
which asking again does not change, so a download that cannot be resumed starts
over. A part sent in chunks says how long it is only by ending, so reading it
fails as it is read: with an error IsRangeIgnored reports where it goes on
past the part, and with io.ErrUnexpectedEOF, which IsTemporary takes as
worth another attempt, where it ends short, since that may be a connection that
dropped:
blob, err := c.Download(ctx, accountID, blobID, &jmapc.DownloadOptions{From: written})
if jmapc.IsRangeIgnored(err) {
// The server does not do ranges. Download the whole blob instead.
written = 0
blob, err = c.Download(ctx, accountID, blobID, nil)
}A server offering urn:ietf:params:jmap:blob can also create and read blobs
through the API, which the endpoints cannot: Blob/upload puts a blob in the
same request as the call that uses it, so the id never comes back to the client
in between.