Envoyer un e-mail
`emails.send` : un message, maintenant ou plus tard.
emails.send
from openemail import openemail email = openemail.emails.send({ 'from': {'email': '[email protected]', 'name': 'Acme Billing'}, 'to': ['[email protected]', 'Grace <[email protected]>'], 'cc': '[email protected]', 'bcc': [{'email': '[email protected]'}], 'replyTo': '[email protected]', 'subject': 'Your September invoice', 'html': '<p>Invoice attached.</p>', 'text': 'Invoice attached.', 'headers': {'X-Campaign': 'invoices'}, 'attachments': [{'filename': 'invoice.pdf', 'content': pdf_bytes}], 'threadId': 'thread_…', 'scheduledAt': 'PT1H', 'tags': {'order': '4021'}, 'tracking': {'opens': True, 'clicks': True},})to, cc et bcc acceptent un destinataire ou plusieurs, et un destinataire seul est enveloppé pour vous. Chacun peut être une adresse nue, Name <addr@host>, ou {'email': ..., 'name': ...}.
Paramètres
fromRecipientInputobligatoire- L'expéditeur. Une adresse nue, `Name <addr@host>`, ou un dictionnaire. Ce doit être une adresse depuis laquelle cette clé peut envoyer. Il n'y a pas d'expéditeur de repli : un envoi nomme donc toujours l'adresse depuis laquelle il part.
toRecipientInput | list[RecipientInput]obligatoire- Un destinataire ou plusieurs ; un destinataire seul est enveloppé pour vous. 50 au maximum pour to, cc et bcc réunis.
ccRecipientInput | list[RecipientInput]- Compte dans la limite de 50 destinataires.
bccRecipientInput | list[RecipientInput]- Jamais nommé dans les octets que reçoivent les autres, car une enveloppe est transmise par destinataire.
replyToRecipientInput- Une seule adresse, envoyée comme en-tête Reply-To.
subjectstr- 998 caractères au maximum, la limite de ligne RFC 5322. Vide par défaut.
htmlstr- L'un de html, text, draftId ou template est obligatoire. C'est le HTML que voient les destinataires quand html et text sont tous deux fournis.
textstr- La partie en texte brut.
templateEmailSendTemplate- Rend un template stocké côté serveur. `version` épingle une révision ; omettez-le pour utiliser ce qui est publié au moment où la requête est acceptée. Une prop inconnue ou manquante donne un 422 plutôt qu'un blanc dans le message.
draftIdstr- Envoie un brouillon enregistré sous cette enveloppe.
headersdict[str, str]- `X-*`, `List-*`, Reply-To, Precedence, Auto-Submitted, Importance, Priority et Feedback-Id. Tout ce que le transport définit lui-même est refusé plutôt qu'ignoré en silence.
attachmentslist[AttachmentInput]- `{'filename': ..., 'content': ...}` avec un `'contentType'` facultatif, ou `{'fileId': ...}` désignant un fichier déjà présent dans l'espace de travail, comme un fichier issu de `files.upload`. Transmettez des octets pour content et ils sont encodés en base64 pour vous. 20 fichiers, les fichiers inline étant plafonnés à 5 Mo au total une fois décodés. Un fichier stocké peut être plus gros et voyage sous forme de lien de téléchargement.
attachmentDeliveryAttachmentDeliveryMode- `mime`, `link` ou `auto`. `auto` transporte les fichiers sous forme de liens de téléchargement dès qu'ils dépassent 2 Mo sur un domaine disposant d'un domaine de fichiers actif, et à l'intérieur du message sinon. Omis, c'est le réglage de la boîte qui s'applique, et il vaut `auto` par défaut.
threadIdstr- Répond dans un thread existant. Le transport écrit In-Reply-To et References.
scheduledAtdatetime | str- Un `datetime`, un instant ISO-8601, ou une durée comme `PT1H`. Jusqu'à un an à l'avance, jamais dans le passé. Ne peut pas être combiné avec cancellableForSeconds.
cancellableForSecondsint- De 0 à 900. Une fenêtre d'annulation sur un envoi immédiat : le mécanisme d'annulation du composer, exposé plutôt que codé en dur.
tagsdict[str, str]- Jusqu'à 10 libellés, renvoyés tels quels et filtrables. Jamais interprétés.
signaturebool- Indique si ce message porte la signature de l'adresse d'envoi : la sienne, sinon celle du catch-all pour une adresse captée par un catch-all, sinon le pied de page OpenEmail, sauf si cette adresse l'a désactivé. Sans valeur, un corps `html` part exactement tel qu'écrit, sans signature, et un corps uniquement `text` la porte. Indiquez `False` pour le courrier qu'un programme envoie au nom de quelqu'un, comme un reçu, une réinitialisation de mot de passe ou un résumé, sous lesquels personne ne veut de signature personnelle.
trackingTrackingRequest- Indique s'il faut ajouter un pixel d'ouverture et réécrire les liens pour ce message. Désactivé sauf si le suivi a été activé pour l'adresse d'expédition (ou le catch-all qui l'a attrapée) ; chacun des deux champs indiqué ici tranche pour ce seul message, quel que soit le réglage de l'adresse.
translateSendTranslateOptions- L'envoie dans la langue du destinataire. `to` accepte un code, un nom anglais ou le nom de la langue dans sa propre langue ; `subject` et `includeOriginal` valent true par défaut. Résolu au moment où la requête est acceptée, pour qu'un message planifié porte les mots qui ont été approuvés. Refusé en présence de `draftId`.
Réponse
idstr- L'id de l'envoi, `msg_…`. Utilisez-le pour `get`, `cancel`, `reschedule` et `get_tracking`.
statusEmailStatus- queued, scheduled, sending, sent, partial, bounced, cancelled ou failed. Lisez ceci plutôt que de vous fier au simple retour de l'appel. `partial` est un état à part entière : certains destinataires l'ont reçu et on ne peut pas le leur reprendre, réessayer est donc une erreur et annoncer un échec est un mensonge.
modeApiKeyMode- Quel type de clé l'a envoyé. Un envoi de test est enregistré et jamais transmis.
fromstr- L'adresse réellement autorisée et placée sur le réseau, qui n'est pas toujours celle demandée.
subjectstr | None- Tel qu'envoyé.
messageIdstr | None- Le Message-ID RFC 5322. Null tant que le MIME n'existe pas. Le service d'envoi réécrit l'en-tête au départ : aucun bounce ni rapport de distribution ne porte donc cette valeur. C'est sur `id` qu'un événement revient.
threadIdstr | None- Le thread dans lequel il a atterri.
transportEmailTransport | str | None- Par où le message est parti. Null jusqu'à l'expédition.
attemptsint- Combien de fois l'expédition a été tentée.
lastErrorstr | None- Pourquoi la dernière tentative a échoué, mot pour mot.
scheduledAtstr | None- L'instant ISO auquel il doit partir.
cancellableUntilstr | None- Tant que l'heure actuelle est antérieure, cancel fonctionne encore.
sentAtstr | None- L'instant ISO auquel il est parti.
tagsdict[str, str]- Ce que vous avez envoyé, renvoyé tel quel.
sourceEmailSource | str- composer, api, mcp, ai, oauth ou form : quelle surface a demandé l'envoi. `api`, c'est ce client avec une clé API, et `oauth`, ce client avec un jeton d'accès.
createdAtstr- L'instant ISO auquel l'enregistrement a été écrit.
replayedbool- Vrai quand une Idempotency-Key correspondait à un envoi qui existait déjà. Rien de nouveau n'a été envoyé, et voici le message d'origine.
translationNotRequired[EmailTranslationResource]- Présent uniquement sur un message qui a été traduit, et uniquement là où toute la requête stockée est transportée : cette réponse et `get`. Un dictionnaire de `language`, `languageName`, `detectedSourceLanguage`, `subject` et `includeOriginal`, des codes plutôt que des lignes de langue. Une ligne de liste ne le porte jamais : son absence là-bas ne dit rien, ni dans un sens ni dans l'autre. Lisez-le avec `email.get('translation')`.
Dans la langue du destinataire
translate rédige le message dans la langue de quelqu'un d'autre avant son départ. Le corps, et l'objet sauf si vous le désactivez, est traduit au moment où l'API accepte la requête, et ce qui en est sorti est ce qui part : une traduction qui n'a pas pu être produite fait refuser l'envoi plutôt que d'expédier le message dans la langue où vous l'avez écrit.
from openemail import openemail email = openemail.emails.send({ 'from': '[email protected]', 'to': '[email protected]', 'subject': 'Your September invoice', 'html': '<p>Invoice attached. Payment is due on the 14th.</p>', 'translate': {'to': 'de'},}) print(email.get('translation'))Personne n'a relu ce texte avant son départ. emails.translate est le même aller-retour, arrêté une étape plus tôt. Montrez-le à une personne, laissez-la le modifier, puis envoyez ce qu'elle a approuvé sans aucun translate sur l'appel. Le repasser traduirait une deuxième fois et jetterait ses corrections.
from openemail import openemail preview = openemail.emails.translate({ 'subject': 'Your September invoice', 'html': '<p>Invoice attached. Payment is due on the 14th.</p>', 'to': 'de',}) print(preview['language']['native'], preview['detectedSourceLanguage'])print(preview['html']) approved_subject = input(f"Subject [{preview['subject']}]: ") or preview['subject'] or '' openemail.emails.send({ 'from': '[email protected]', 'to': '[email protected]', 'subject': approved_subject, 'html': preview['html'] or '',})from openemail import LANGUAGES, is_rtl_language, language_by_code, openemail, resolve_language current = openemail.languages.list() german = resolve_language('Deutsch')traditional = resolve_language('zh-TW')upper = language_by_code('DE') assert len(LANGUAGES) == 200assert german is not None and german['code'] == 'de'assert traditional is not None and traditional['code'] == 'zh-Hant'assert upper is not None and upper['native'] == 'Deutsch'assert is_rtl_language('ar')La table est embarquée dans le paquet, dans l'ordre du sélecteur, pour qu'un sélecteur puisse être rempli avant la première requête. languages.list() renvoie les mêmes lignes depuis le réseau sous forme de simple liste, pour un appelant qui préfère les lignes actuelles à celles livrées avec cette version. resolve_language accepte un code, un nom anglais, un endonyme ou un alias (zh-TW est l'alias d'un code qui n'est plus listé), language_by_code fait correspondre un code exact sans tenir compte de la casse, et seize des lignes s'écrivent de droite à gauche. Cherchez dans native, label et code à la fois, affichez native en premier, et stockez le code.
emails.translate n'est pas réessayé automatiquement. Il consomme des appels au modèle et n'écrit rien : il n'y a donc rien à rendre idempotent, et une retentative après une requête restée sans réponse ne ferait qu'acheter deux fois la même réponse.
- Une langue qui ne se résout à rien donne un
validation_errorsurtranslate.to, avant que quoi que ce soit ne soit envoyé. translation_too_longau-delà de 30 000 caractères,translation_not_configuredquand l'installation n'a aucune IA configurée, un 429ai_quota_exceededquand l'espace de travail a épuisé les actions IA du jour (il se réinitialise à minuit UTC et n'est pas réessayé),translation_failedquand le fournisseur n'a pas répondu. Aucun d'eux n'envoie le message non traduit en guise de repli.- Fonctionne avec
template: c'est la sortie RENDUE qui est traduite, si bien qu'un seul corps stocké sert toutes les langues que lisent vos clients. Un template qui rend un document entier conserve son doctype, ses blocs<style>et ses règles@font-face: seul le corps part vers le modèle et le reste est remis autour. Son<title>est laissé tel quel, et rien ne l'affiche de toute façon. - Une retentative ne coûte rien de plus. La traduction ne fait pas partie de l'empreinte d'idempotence (la requête, elle, en fait partie,
translatecompris) : réessayer un envoi resté sans réponse avec la mêmeIdempotency-Keyrejoue donc le message qui existe déjà au lieu d'en traduire et d'en envoyer un second. - Un message traduit en file ou planifié est figé quant à sa formulation.
emails.reschedulele déplace toujours ; changer ce qu'il dit suppose de l'annuler et de l'envoyer à nouveau.
Pièces jointes
content circule en base64. Transmettez des octets et ils sont encodés pour vous.
from pathlib import Path from openemail.types import AttachmentInput attachments: list[AttachmentInput] = [ { 'filename': 'invoice.pdf', 'content': Path('invoice.pdf').read_bytes(), 'contentType': 'application/pdf', },]to_base64 est exporté si vous en avez besoin ailleurs. Une str dans content est envoyée telle quelle : elle doit donc déjà être en base64.