[PYTHON] Fügen Sie GitHub per E-Mail ein neues Problem hinzu (Amazon SES-Nutzungsversion)

Einführung

Verwenden Sie zuvor den E-Mail-Dienst [^ iftttemail] von IFTTT als E-Mail-Empfänger, rufen Sie die GitHub-API [^ githubapi] über AWS Lambda auf und Fügen Sie GitHub per E-Mail ein neues Problem hinzu / items / ff516aa90eb87c5140e7) Ich habe eine Funktion erstellt. Es ist eigentlich sehr praktisch, ein GitHub-Problem mit einer einzigen E-Mail erstellen zu können, wenn Sie einen Fehler oder eine Verbesserung Ihres eigenen Dienstes bemerken. Ich denke, ich werde es auch in Zukunft weiter verwenden, daher habe ich es neu erstellt, damit es unter AWS funktioniert, einschließlich des E-Mail-Empfängers.

Politik

Verwenden Sie Amazon SES (Simple Email Service) [^ ses] als E-Mail-Empfänger. Indem Sie das E-Mail-Zustellungsziel der von Ihnen verwalteten Domäne an den empfangenden Endpunkt von SES weiterleiten, können Sie E-Mails an Amazon SES → Amazon S3 → AWS Lambda und Bucket Relay senden. Ich habe eine Lambda-Funktion implementiert, die dem GitHub-Repository ein Problem hinzufügt, indem ich die GitHub-API [^ githubapi] gemäß dem Inhalt der E-Mail unter Verwendung des Python-Frameworks AWS Chalice [^ chalice] aufrufe.

Das Implementierungsverfahren ist ungefähr wie folgt.

  1. ** Domänenverwaltung **: Legen Sie den SES-E-Mail-Empfangsendpunkt im E-Mail-Zustellungsziel (MX-Datensatz) fest [^ dns]
  2. ** S3 **: Richten Sie einen S3-Bucket ein, um von SES empfangene E-Mails zu speichern
  3. ** SES **: Erstellen Sie eine eingehende Regel, um eingehende E-Mails im S3-Bucket zu speichern
  4. ** GitHub **: Stellen Sie ein Zugriffstoken ("Repo" -Autorisierung) aus, um die GitHub-API zu verwenden
  5. ** Lambda **: Implementieren und implementieren Sie eine Funktion zum Lesen eingehender E-Mails aus dem S3-Bucket und zum Hinzufügen von Problemen zum GitHub-Repository
  6. ** S3 **: Legen Sie im S3-Bucket ein Ereignis fest, um die bereitgestellte Lambda-Funktion beim Speichern empfangener E-Mails auszuführen.

Informationen zu 1-3 finden Sie im AWS Developer Guide "Empfangen von E-Mails mit Amazon SES-Amazon Simple Email Service -email.html) “und Supportinformationen“ [E-Mails mit Amazon SES in Amazon S3 empfangen und speichern](https://aws.amazon.com/jp/premiumsupport/knowledge-center/ses- eingehende E-Mails erhalten /) “wird detailliert beschrieben. Für 4 wird das spezifische Verfahren unter "So legen Sie GitHub" Persönliche Zugriffstoken "- Qiita" erläutert.

In diesem Artikel fasse ich die Implementierungen von 5 und 6 unten zusammen.

Implementierung

Wenn Sie in den obigen Abschnitten 5 und 6 neue eingehende E-Mails im S3-Bucket speichern, lesen Sie die empfangenen E-Mails aus dem S3-Bucket und fügen Sie das Problem dem GitHub-Repository hinzu. Dieser Prozess, Chalice [^ chalice], ein Python-Framework für die Lambda-basierte Entwicklung, kann sehr einfach mit einem Dekorator namens "on_s3_event" erreicht werden.

Chalice.on_s3_event() S3 verfügt über einen Mechanismus zum Senden einer Benachrichtigung an Lambda usw., wenn sich Änderungen am Bucket ergeben. Um diesen Mechanismus verwenden zu können, muss ein Ereignis festgelegt werden, um die Benachrichtigung in S3 zu überspringen und eine Funktion zum Empfangen der Benachrichtigung in Lambda zu erstellen. Wenn Sie jedoch Chalice verwenden, werden diese Einstellungen fast automatisch vorgenommen.

Der Basiscode, der die Lambda-Funktion implementiert, die S3-Ereignisse in Chalice empfängt, lautet [^ 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 () Wenn Sie eine Funktion mit einem Dekorateur definieren und den Code schreiben, werden beim Bereitstellen der Funktion auf Lambda mit chalice deploy alle Rollen und Ereignisse für S3 und Lambda automatisch festgelegt. Ich werde.

Code

In dieser Funktion mit dem Dekorator "Chalice.on_s3_event ()" habe ich dieses Mal den Vorgang des Lesens der empfangenen E-Mails aus dem S3-Bucket [^ email] und des Hinzufügens des Problems zum GitHub-Repository beschrieben. Der Hauptcode von Chalice, app.py, lautet wie folgt.

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)

    #E-Mail analysieren
    msg = email.message_from_bytes(obj)
    msg_from = get_header(msg, 'From')
    msg_subject = get_header(msg, 'Subject')
    msg_content = get_content(msg)

    #E-Mail-Adresse extrahieren
    pattern = "[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+"
    adds = re.findall(pattern, msg_from)
    #Extrahieren Sie die Einstellungen entsprechend der E-Mail-Adresse
    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

    #Holen Sie sich ein Repository
    repos = getRepositories(config['GITHUB_ACCESS_TOKEN'])
    logger.info('repositories: {}'.format(repos))

    #Bestimmen Sie das Repository anhand des E-Mail-Titels
    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))

    #POST-Problem
    postIssue(
        config['GITHUB_ACCESS_TOKEN'],
        config['GITHUB_OWNER'],
        repo, title, msg_content
    )

    #Mail löschen
    deleteS3Object(os.environ.get('BUCKET_NAME'), event.key)


#Objekt aus S3 abrufen
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

#S3-Objekt löschen
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))


#Mail-Header abrufen
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

#E-Mail-Text erhalten
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


#Holen Sie sich eine Liste der Github-Repositorys
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)

#Problem zum Github-Repository hinzufügen
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)

Die folgende Einstellungsdatei wird aus S3 gelesen, damit das Zugriffstoken für die Verwendung der GitHub-API entsprechend der E-Mail-Adresse des Absenders umgeschaltet werden kann.

mail2issue-config.json


{
    "<Absender-E-Mail-Adresse>": {
        "GITHUB_OWNER": "<GitHub-Benutzername>",
        "GITHUB_ACCESS_TOKEN": "<GitHub-Zugriffstoken>",
        "GITHUB_DEFAULT_REPOSITORY": "<Repository-Name, falls nicht im E-Mail-Titel angegeben>"
    },
    ...
}

abschließend

Wenn ich Amazon SES zu einem anderen Zweck berührt habe und E-Mails über AWS erhalten kann, habe ich mir dieses Refactoring ausgedacht. Es gibt immer noch viele E-Mail-ausgelöste Dienste, daher werden wir weiterhin überlegen, dieses Muster anzuwenden.

Recommended Posts

Fügen Sie GitHub per E-Mail ein neues Problem hinzu (Amazon SES-Nutzungsversion)
[Morphologische Analyse] So fügen Sie Mecab ein neues Wörterbuch hinzu
Fügen Sie Heatrapy eine Funktion hinzu, die Wärme + Wärme bei Temperatur übertragen kann