[PYTHON] Ajouter un nouveau problème à GitHub par e-mail (version d'utilisation d'Amazon SES)

introduction

Auparavant, en utilisant le service de messagerie d'IFTTT [^ iftttemail] comme récepteur de messagerie, accédez à l'API GitHub [^ githubapi] via AWS Lambda et Ajouter un nouveau problème à GitHub par courrier / items / ff516aa90eb87c5140e7) J'ai créé une fonction. Il est en fait très pratique de pouvoir créer un problème GitHub avec un seul e-mail lorsque vous remarquez un bogue ou une amélioration de votre propre service. Je pense que je continuerai à l'utiliser à l'avenir, alors je l'ai refait pour qu'il fonctionne sur AWS, y compris le récepteur de messagerie.

politique

Utilisez Amazon SES (Simple Email Service) [^ ses] comme destinataire du courrier. En dirigeant la destination de distribution du courrier du domaine que vous gérez vers le point de terminaison de réception de SES, vous pouvez envoyer du courrier à Amazon SES → Amazon S3 → AWS Lambda et au relais de compartiment. J'ai implémenté la fonction Lambda qui ajoute un problème au référentiel GitHub en accédant à l'API GitHub [^ githubapi] en fonction du contenu de l'e-mail, en utilisant le framework Python AWS Chalice [^ calice].

La procédure de mise en œuvre est à peu près la suivante.

  1. ** Gestion du domaine **: définissez le point de terminaison de réception des e-mails SES dans la destination de remise des e-mails (enregistrement MX) [^ dns]
  2. ** S3 **: configurez un compartiment S3 pour stocker les e-mails reçus par SES
  3. ** SES **: créez une règle entrante pour stocker les e-mails entrants dans le compartiment S3
  4. ** GitHub **: émettez un jeton d'accès (autorisation "repo") pour utiliser l'API GitHub
  5. ** Lambda **: implémentez et déployez une fonction qui lit le courrier entrant à partir d'un compartiment S3 et ajoute un problème au référentiel GitHub.
  6. ** S3 **: définissez un événement dans le compartiment S3 pour exécuter la fonction Lambda déployée lors de l'enregistrement du courrier reçu.

Pour 1-3, consultez le AWS Developer Guide "[Réception d'e-mails à l'aide d'Amazon SES-Amazon Simple Email Service](https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/receiving] -email.html) »et informations d'assistance« [Recevoir et enregistrer des e-mails sur Amazon S3 à l'aide d'Amazon SES](https://aws.amazon.com/jp/premiumsupport/knowledge-center/ses- receive-inbound-emails /) »est détaillé. Pour 4, la procédure spécifique est expliquée dans "Comment configurer GitHub" Jetons d'accès personnels "--Qiita".

Donc, dans cet article, je vais résumer les implémentations de 5 et 6 ci-dessous.

la mise en oeuvre

Ce que vous voulez faire dans 5 et 6 ci-dessus est, après tout, lorsqu'un nouveau courrier entrant est enregistré dans le compartiment S3, lire le courrier reçu à partir du compartiment S3 et ajouter le problème au référentiel GitHub. Ce processus, Chalice [^ calice], un framework Python pour le développement basé sur Lambda, peut être réalisé très facilement en utilisant un décorateur appelé «on_s3_event».

Chalice.on_s3_event() S3 dispose d'un mécanisme pour envoyer une notification à Lambda, etc. en cas de modification dans le compartiment. Pour utiliser ce mécanisme, il est nécessaire de définir un événement pour ignorer la notification dans S3 et créer une fonction pour recevoir la notification dans Lambda, mais si vous utilisez Chalice, ces paramètres seront effectués presque automatiquement.

Le code de base qui implémente la fonction Lambda qui reçoit les événements S3 dans Chalice est [^ on_s3_event].

app.py(sample)


from chalice import Chalice

app = chalice.Chalice(app_name='s3eventdemo')
app.debug = True

@app.on_s3_event(bucket='mybucket-name',
                 events=['s3:ObjectCreated:*'])
def handle_s3_event(event):
    app.log.debug("Received event for bucket: %s, key: %s",
                  event.bucket, event.key)

Chalice.on_s3_event () Si vous définissez une fonction avec un décorateur et écrivez du code, lorsque vous déployez la fonction sur Lambda avec chalice deploy, tous les rôles et paramètres d'événement pour S3 et Lambda seront effectués automatiquement. Je vais.

code

Donc, cette fois, dans cette fonction avec le décorateur Chalice.on_s3_event (), j'ai décrit le processus de lecture du courrier reçu depuis le compartiment S3 [^ email] et l'ajout du problème au référentiel GitHub. Le code principal de Chalice, ʻapp.py`, est le suivant.

app.py


from chalice import Chalice
import logging, os, json, re
import boto3
from botocore.exceptions import ClientError
import email
from email.header import decode_header
from email.utils import parsedate_to_datetime
import urllib.request


# setup chalice
app = Chalice(app_name='mail2issue')
app.debug = False

# setup logger
logger = logging.getLogger()
logger.setLevel(logging.INFO)
logformat = (
    '[%(levelname)s] %(asctime)s.%(msecs)dZ (%(aws_request_id)s) '
    '%(filename)s:%(funcName)s[%(lineno)d] %(message)s'
)
formatter = logging.Formatter(logformat, '%Y-%m-%dT%H:%M:%S')
for handler in logger.handlers:
    handler.setFormatter(formatter)


# on_s3_event
@app.on_s3_event(
    os.environ.get('BUCKET_NAME'),
    events = ['s3:ObjectCreated:*'],
    prefix = os.environ.get('BUCKET_KEY_PREFIX')
)
def receive_mail(event):
    logger.info('received key: {}'.format(event.key))

    # read S3 object (email message)
    obj = getS3Object(os.environ.get('BUCKET_NAME'), event.key)
    if obj is None:
        logger.warning('object not found!')
        return

    # read S3 object (config)
    config = getS3Object(os.environ.get('BUCKET_NAME'), 'mail2issue-config.json')
    if config is None:
        logger.warning('mail2issue-config.json not found!')
        return
    settings = json.loads(config)

    #Analyser les e-mails
    msg = email.message_from_bytes(obj)
    msg_from = get_header(msg, 'From')
    msg_subject = get_header(msg, 'Subject')
    msg_content = get_content(msg)

    #Extraire l'adresse e-mail
    pattern = "[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+"
    adds = re.findall(pattern, msg_from)
    #Extraire les paramètres correspondant à l'adresse e-mail
    config = None
    for add in settings:
        if add in adds:
            config = settings[add]
            break
    if config is None:
        logger.info('there is no config for {}'.format(', '.join(adds)))
        return

    #Obtenez un référentiel
    repos = getRepositories(config['GITHUB_ACCESS_TOKEN'])
    logger.info('repositories: {}'.format(repos))

    #Déterminez le référentiel à partir du titre de l'e-mail
    repo = config['GITHUB_DEFAULT_REPOSITORY']
    title = msg_subject
    spaceIdx = msg_subject.find(' ')
    if spaceIdx > 0:
        repo_tmp = msg_subject[0:spaceIdx]
        if repo_tmp in repos:
            title = msg_subject[spaceIdx+1:]
            repo = repo_tmp
    title = title.lstrip().rstrip()
    logger.info("repository: '{}'".format(repo))
    logger.info("title: '{}'".format(title))

    #Problème POST
    postIssue(
        config['GITHUB_ACCESS_TOKEN'],
        config['GITHUB_OWNER'],
        repo, title, msg_content
    )

    #Supprimer le courrier
    deleteS3Object(os.environ.get('BUCKET_NAME'), event.key)


#Obtenir un objet de S3
def getS3Object(bucket, key):
    ret = None
    s3obj = None
    try:
        s3 = boto3.client('s3')
        s3obj = s3.get_object(
            Bucket = bucket,
            Key = key
        )
    except ClientError as e:
        logger.warning('S3 ClientError: {}'.format(e))
    if s3obj is not None:
        ret = s3obj['Body'].read()
    return ret

#Supprimer l'objet S3
def deleteS3Object(bucket, key):
    try:
        s3 = boto3.client('s3')
        s3.delete_object(
            Bucket = bucket,
            Key = key
        )
    except ClientError as e:
        logger.warning('S3 ClientError: {}'.format(e))


#Obtenir l'en-tête du courrier
def get_header(msg, name):
    header = ''
    if msg[name]:
        for tup in decode_header(str(msg[name])):
            if type(tup[0]) is bytes:
                charset = tup[1]
                if charset:
                    header += tup[0].decode(tup[1])
                else:
                    header += tup[0].decode()
            elif type(tup[0]) is str:
                header += tup[0]
    return header

#Obtenir le corps de l'e-mail
def get_content(msg):
    charset = msg.get_content_charset()
    payload = msg.get_payload(decode=True)
    try:
        if payload:
            if charset:
                return payload.decode(charset)
            else:
                return payload.decode()
        else:
            return ""
    except:
        return payload


#Obtenir une liste des dépôts github
def getRepositories(token):
    req = urllib.request.Request(
        'https://api.github.com/user/repos',
        method = 'GET',
        headers = {
            'Authorization': 'token {}'.format(token)
        }
    )
    repos = []
    try:
        with urllib.request.urlopen(req) as res:
            for repo in json.loads(res.read().decode('utf-8')):
                repos.append(repo['name'])
    except Exception as e:
        logger.exception("urlopen error: %s", e)
    return set(repos)

#Ajouter un problème au référentiel github
def postIssue(token, owner, repository, title, content):
    req = urllib.request.Request(
        'https://api.github.com/repos/{}/{}/issues'.format(owner, repository),
        method = 'POST',
        headers = {
            'Content-Type': 'application/json',
            'Authorization': 'token {}'.format(token)
        },
        data = json.dumps({
            'title': title,
            'body': content
        }).encode('utf-8'),
    )
    try:
        with urllib.request.urlopen(req) as res:
            logger.info(res.read().decode("utf-8"))
    except Exception as e:
        logger.exception("urlopen error: %s", e)

Le fichier de paramètres suivant est lu à partir de S3 afin que le jeton d'accès pour l'utilisation de l'API GitHub puisse être changé en fonction de l'adresse e-mail de l'expéditeur.

mail2issue-config.json


{
    "<Adresse e-mail de l'expéditeur>": {
        "GITHUB_OWNER": "<Nom d'utilisateur GitHub>",
        "GITHUB_ACCESS_TOKEN": "<Jeton d'accès GitHub>",
        "GITHUB_DEFAULT_REPOSITORY": "<Nom du référentiel s'il n'est pas spécifié dans le titre de l'e-mail>"
    },
    ...
}

en conclusion

Si je touchais à Amazon SES dans un autre but et que je pouvais recevoir des e-mails sur AWS, j'ai proposé cette refactorisation. Il existe encore de nombreux services déclenchés par e-mail, nous continuerons donc à envisager d'appliquer ce modèle.

Recommended Posts

Ajouter un nouveau problème à GitHub par e-mail (version d'utilisation d'Amazon SES)
[Analyse morphologique] Comment ajouter un nouveau dictionnaire à Mecab
Ajoutez une fonction à heatrapy qui peut transférer chaleur + chaleur à température