[PYTHON] J'ai réussi à le faire parce que la coutume de joindre un zip avec un mot de passe à un e-mail et de dire «Je vous enverrai le mot de passe séparément» est gênante.

Cette coutume

Il existe une coutume selon laquelle un zip avec un mot de passe est joint à un e-mail et "le mot de passe sera envoyé séparément". Je ne le fais pas moi-même, mais c'est ennuyeux parce que je dois le faire selon l'autre partie.

Ici, les avantages et les inconvénients de cette méthode n'ont pas d'importance. Peu importe combien je prêche, la situation d'avoir cette coutume ne change pas.

Et je ne pense pas à briser cette pratique. Je vais laisser cela à quelque chose avec un pouvoir énorme.

Dit le vieil idiot. "Enroulez-le autour d'un long." Cependant, je pense qu'il vaut mieux réfléchir à la façon de l'enrouler.

Pensez à la façon de remonter intelligemment

Il n'y a qu'une seule chose que je veux résoudre quand elle est enroulée. Ne sois pas ennuyé. Si vous créez un système Web à cet effet et ouvrez le navigateur pour faire quelque chose comme ça, ce sera écrasant. Je veux le réaliser aussi près que possible de la transmission de courrier normale.

Donc, après y avoir réfléchi, j'ai essayé de le résoudre avec un sentiment de sans serveur en utilisant Amazon SES tout en autorisant certaines restrictions.

spécification

  1. Rédigez un e-mail normalement (nouveau, réponse, transfert)
  2. Lancez le fichier tel quel sans le compresser
  3. Définissez l'adresse e-mail de SES sur «À» et la personne à qui vous voulez réellement envoyer le fichier sur «Répondre à».
  4. Croyez au système et appuyez sur le bouton Soumettre
  5. Vous et l'autre partie recevrez un e-mail avec un code zip joint au mot de passe et un e-mail de notification de mot de passe.

Cependant, il existe les restrictions suivantes. Personnellement, c'est acceptable.

Configuration du système

flow_01.png

  1. Envoyez un e-mail à SES
  2. Les données de messagerie sont enregistrées dans S3
  3. Lambda commence avec lui comme déclencheur
  4. Lambda analyse l'e-mail et génère un mot de passe et un fichier zip
  5. Envoyez un joli email (envoyez-le à vous-même avec Cci au cas où)

la mise en oeuvre

Lambda C'est la première fois que j'écris sérieusement python, mais est-ce que ça va comme ça? Il s'agit d'une bataille entre les e-mails, les codes de caractères et les fichiers.

# -*- coding: utf-8 -*-

import os
import sys
import string
import random
import json
import urllib.parse
import boto3
import re
import smtplib
import email
import base64
from email                import encoders
from email.header         import decode_header
from email.mime.base      import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text      import MIMEText
from email.mime.image     import MIMEImage
from datetime             import datetime

sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'vendored'))
import pyminizip

s3 = boto3.client('s3')

class MailParser(object):
    """
Classe d'analyse du courrier
    (référence) http://qiita.com/sayamada/items/a42d344fa343cd80cf86
    """

    def __init__(self, email_string):
        """
Initialisation
        """
        self.email_message    = email.message_from_string(email_string)
        self.subject          = None
        self.from_address     = None
        self.reply_to_address = None
        self.body             = ""
        self.attach_file_list = []

        #Interprétation de eml
        self._parse()

    def get_attr_data(self):
        """
Obtenir des données de messagerie
        """
        attr = {
                "from":         self.from_address,
                "reply_to":     self.reply_to_address,
                "subject":      self.subject,
                "body":         self.body,
                "attach_files": self.attach_file_list
                }
        return attr


    def _parse(self):
        """
Analyse des fichiers courrier
        """

        #Analyse de la partie en-tête du message
        self.subject          = self._get_decoded_header("Subject")
        self.from_address     = self._get_decoded_header("From")
        self.reply_to_address = self._get_decoded_header("Reply-To")

        #Extraire uniquement la chaîne de caractères de l'adresse e-mail
        from_list =  re.findall(r"<(.*@.*)>", self.from_address)
        if from_list:
            self.from_address = from_list[0]
        reply_to_list =  re.findall(r"<(.*@.*)>", self.reply_to_address)
        if reply_to_list:
            self.reply_to_address = ','.join(reply_to_list)

        #Analyse de la partie du corps du message
        for part in self.email_message.walk():
            #Si le ContentType est en plusieurs parties, le contenu réel est encore plus
            #Puisqu'il est dans la partie intérieure, sautez-le
            if part.get_content_maintype() == 'multipart':
                continue
            #Obtenir le nom du fichier
            attach_fname = part.get_filename()
            #Doit être le corps s'il n'y a pas de nom de fichier
            if not attach_fname:
                charset = str(part.get_content_charset())
                if charset != None:
                    if charset == 'utf-8':
                        self.body += part.get_payload()
                    else:
                        self.body += part.get_payload(decode=True).decode(charset, errors="replace")
                else:
                    self.body += part.get_payload(decode=True)
            else:
                #S'il y a un nom de fichier, c'est un fichier joint
                #Obtenez des données
                self.attach_file_list.append({
                    "name": attach_fname,
                    "data": part.get_payload(decode=True)
                })

    def _get_decoded_header(self, key_name):
        """
Obtenez le résultat décodé à partir de l'objet d'en-tête
        """
        ret = ""

        #Les clés qui n'ont pas l'élément correspondant renvoient des caractères vides
        raw_obj = self.email_message.get(key_name)
        if raw_obj is None:
            return ""
        #Rendre le résultat décodé unicode
        for fragment, encoding in decode_header(raw_obj):
            if not hasattr(fragment, "decode"):
                ret += fragment
                continue
            #S'il n'y a pas d'encodage, UTF pour le moment-Décoder avec 8
            if encoding:
                ret += fragment.decode(encoding)
            else:
                ret += fragment.decode("UTF-8")
        return ret

class MailForwarder(object):

    def __init__(self, email_attr):
        """
Initialisation
        """
        self.email_attr = email_attr
        self.encode     = 'utf-8'

    def send(self):
        """
Compressez le fichier joint avec un mot de passe, transférez-le et envoyez un e-mail de notification de mot de passe
        """

        #Génération de mot de passe
        password = self._generate_password()

        #génération de données zip
        zip_name = datetime.now().strftime('%Y%m%d%H%M%S')
        zip_data = self._generate_zip(zip_name, password)

        #Envoyer des données zip
        self._forward_with_zip(zip_name, zip_data)

        #Envoyer le mot de passe
        self._send_password(zip_name, password)

    def _generate_password(self):
        """
Génération de mot de passe
Mélangez en prenant 4 lettres chacun parmi les symboles, les lettres et les chiffres
        """
        password_chars = ''.join(random.sample(string.punctuation, 4)) + \
                         ''.join(random.sample(string.ascii_letters, 4)) + \
                         ''.join(random.sample(string.digits, 4))

        return ''.join(random.sample(password_chars, len(password_chars)))

    def _generate_zip(self, zip_name, password):
        """
Générer des données pour le fichier Zip avec mot de passe
        """
        tmp_dir  = "/tmp/" + zip_name
        os.mkdir(tmp_dir)

        #Enregistrez le fichier localement
        for attach_file in self.email_attr['attach_files']:
            f = open(tmp_dir + "/" + attach_file['name'], 'wb')
            f.write(attach_file['data'])
            f.flush()
            f.close()

        #Pour compresser avec mot de passe
        dst_file_path = "/tmp/%s.zip" % zip_name
        src_file_names = ["%s/%s" % (tmp_dir, name) for name in os.listdir(tmp_dir)]

        pyminizip.compress_multiple(src_file_names, dst_file_path, password, 4)

        # #Lire le fichier zip généré
        r = open(dst_file_path, 'rb')
        zip_data = r.read()
        r.close()

        return zip_data

    def _forward_with_zip(self, zip_name, zip_data):
        """
Générer des données pour le fichier Zip avec mot de passe
        """
        self._send_message(
                self.email_attr['subject'],
                self.email_attr["body"].encode(self.encode),
                zip_name,
                zip_data
                )
        return

    def _send_password(self, zip_name, password):
        """
Envoyer le mot de passe du fichier zip
        """

        subject = self.email_attr['subject']

        message = """
Il s'agit du mot de passe du fichier que vous avez envoyé précédemment.

[matière] {}
[nom de fichier] {}.zip
[mot de passe] {}
        """.format(subject, zip_name, password)

        self._send_message(
                '[password]%s' % subject,
                message,
                None,
                None
                )
        return

    def _send_message(self, subject, message, attach_name, attach_data):
        """
envoyer un e-mail
        """

        msg = MIMEMultipart()

        #entête
        msg['Subject'] = subject
        msg['From']    = self.email_attr['from']
        msg['To']      = self.email_attr['reply_to']
        msg['Bcc']     = self.email_attr['from']

        #Texte
        body = MIMEText(message, 'plain', self.encode)
        msg.attach(body)

        #Pièce jointe
        if attach_data:
            file_name = "%s.zip" % attach_name
            attachment = MIMEBase('application', 'zip')
            attachment.set_param('name', file_name)
            attachment.set_payload(attach_data)
            encoders.encode_base64(attachment)
            attachment.add_header("Content-Dispositon", "attachment", filename=file_name)
            msg.attach(attachment)

        #Envoyer
        smtp_server   = self._get_decrypted_environ("SMTP_SERVER")
        smtp_port     = self._get_decrypted_environ("SMTP_PORT")
        smtp_user     = self._get_decrypted_environ("SMTP_USER")
        smtp_password = self._get_decrypted_environ("SMTP_PASSWORD")
        smtp = smtplib.SMTP(smtp_server, smtp_port)
        smtp.ehlo()
        smtp.starttls()
        smtp.ehlo()
        smtp.login(smtp_user, smtp_password)
        smtp.send_message(msg)
        smtp.quit()
        print("Successfully sent email")

        return

    def _get_decrypted_environ(self, key):
        """
Décrypter les variables d'environnement chiffrées
        """

        client = boto3.client('kms')
        encrypted_data = os.environ[key]
        return client.decrypt(CiphertextBlob=base64.b64decode(encrypted_data))['Plaintext'].decode('utf-8')

def lambda_handler(event, context):

    #Obtenir le nom du bucket et le nom de la clé de l'événement
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'])

    try:
        #Lire le contenu du fichier depuis S3
        s3_object = s3.get_object(Bucket=bucket, Key=key)
        email_string = s3_object['Body'].read().decode('utf-8')

        #Analyser les e-mails
        parser = MailParser(email_string)

        #Réexpédition du courrier
        forwarder = MailForwarder(parser.get_attr_data())
        forwarder.send()
        return

    except Exception as e:
        print(e)
        raise e

pyminizip Il semble que le zip protégé par mot de passe ne puisse pas être fait avec une bibliothèque standard. Donc, je me suis appuyé sur une bibliothèque externe appelée pyminizip uniquement ici. Cependant, il s'agissait d'une bibliothèque qui a été créée au moment de l'installation pour créer un binaire, j'ai donc configuré un conteneur Docker pour Amazon Linux localement pour l'exécuter sur Lambda et créé un binaire. Y a-t-il un autre bon moyen? ..

AWS SAM En passant, j'ai testé cela localement en utilisant AWS SAM. C'était bien jusqu'à ce que j'essaye d'écrire directement les informations du serveur SMTP, mais quand je les ai déplacées vers la variable d'environnement, cela ne fonctionnait pas bien et j'étais frustré. Il semble qu'il a été corrigé mais pas publié.

Méthode d'introduction

Je vais le publier parce que c'est un gros problème. Nom de code zaru. Veuillez me pardonner si la méthode de réglage reste floue. .. https://github.com/Kta-M/zaru

Je ne l'ai essayé que dans mon environnement (Mac, Thunderbird), donc cela peut ne pas fonctionner en fonction du mailer et d'autres environnements. Veuillez prendre la responsabilité de vos actions.

SES SES n'est pas encore disponible dans la région de Tokyo, nous allons donc le construire dans la région de l'Oregon (us-west-2).

Vérification du domaine

Tout d'abord, nous vérifierons le domaine afin que vous puissiez envoyer des e-mails à SES. Il existe différentes méthodes, je vais donc omettre ce domaine. Par exemple, cela peut être utile-> Envoyer le courrier de domaine à l'aide d'Amazon SES / Route53 with Rails

Création de règles

Après avoir vérifié le domaine, créez une règle.

Dans Ensembles de règles sur le côté droit du menu, cliquez sur Afficher le jeu de règles actif. ses_rule_01.png

Cliquez sur "Créer une règle". ses_rule_02.png

Enregistrez l'adresse e-mail pour recevoir. Saisissez l'adresse e-mail du domaine vérifié et cliquez sur «Ajouter un destinataire». ses_rule_03.png

Enregistrez l'action lors de la réception d'un e-mail. Sélectionnez S3 comme type d'action et spécifiez le compartiment pour stocker les données de courrier reçues. À ce stade, si vous créez un compartiment avec Créer un compartiment S3, la stratégie de compartiment requise sera enregistrée automatiquement, ce qui est pratique. Une stratégie est définie qui autorise les téléchargements de fichiers de SES vers le compartiment.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowSESPuts-XXXXXXXXXXXX",
            "Effect": "Allow",
            "Principal": {
                "Service": "ses.amazonaws.com"
            },
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::<ses-bucket-name>/*",
            "Condition": {
                "StringEquals": {
                    "aws:Referer": "XXXXXXXXXXXX"
                }
            }
        }
    ]
}

En outre, les données de messagerie enregistrées dans le compartiment peuvent être stockées, il peut donc être préférable de définir un cycle de vie afin qu'elles soient supprimées après un certain temps. ses_rule_04.png

Donnez un nom à la règle. Le reste est par défaut. ses_rule_05.png

Vérifiez les détails d'inscription et inscrivez-vous! ses_rule_06.png

Lambda

Déployer

Déployez dans la région de l'Oregon ainsi que dans SES. Étant donné que CloudFormation sera utilisé, veuillez créer un compartiment S3 pour télécharger des données.

# git clone [email protected]:Kta-M/zaru.git
# cd zaru
# aws cloudformation package --template-file template.yaml --s3-bucket <cfn-bucket-name> --output-template-file packaged.yaml
# aws cloudformation deploy --template-file packaged.yaml --stack-name zaru-stack --capabilities CAPABILITY_IAM --region us-west-2

Si vous accédez à la console Lambda, la fonction est créée. Il crée également les rôles IAM nécessaires pour exécuter cette fonction. lambda_01.png

Réglage du déclencheur

Configurez Lambda pour qu'il fonctionne en déclenchant l'entrée des données de messagerie dans le compartiment.

Accédez à l'onglet Déclenchement sur l'écran des détails de la fonction. lambda_02.png

Cliquez sur «Ajouter un déclencheur» pour créer un événement S3. Le compartiment dont les données proviennent de SES, le type d'événement est Put. A part cela, c'est la valeur par défaut. Le seau estLamb_03.png

Créer une clé de chiffrement

Dans cette fonction Lambda, les informations relatives à SMTP sont obtenues à partir de la variable d'environnement chiffrée. Créez une clé à utiliser pour ce cryptage.

Depuis la console IAM, cliquez sur la «clé de chiffrement» en bas à gauche. Changez la région en Oregon et créez la clé. lambda_04.png

Tout ce que vous avez à faire est de définir un alias de votre choix, et le reste est OK par défaut. lambda_05.png

Définition du nombre de variables d'environnement

Revenez à Lambda et définissez les variables d'environnement utilisées dans la fonction. Au bas de l'onglet Code se trouve un formulaire pour définir les variables d'environnement. Cochez «Activer l'assistant de chiffrement» et spécifiez la clé de chiffrement que vous avez créée précédemment. Pour les variables d'environnement, entrez le nom et la valeur de la variable (texte brut) et appuyez sur le bouton «cryptage». Ensuite, il sera chiffré avec la clé de chiffrement spécifiée. Les quatre variables d'environnement suivantes sont définies.

Nom de variable La description Exemple
SMTP_SERVER serveur smtp smtp.example.com
SMTP_PORT port smtp 587
SMTP_USER Nom d'utilisateur pour se connecter au serveur smtp [email protected]
SMTP_PASSWORD SMTP_Mot de passe de l'utilisateur

lambda_06.png

Paramètres de rôle

Enfin, accordez au rôle qui exécute cette fonction Lambda les autorisations requises.

Tout d'abord, accédez à la stratégie de la console IAM et créez les deux stratégies suivantes avec Créer une stratégie-> Créer votre propre stratégie. lambda_07.png

** Politique: s3-get-object-zaru ** Pour «», spécifiez le nom du compartiment pour recevoir les données de messagerie de SES.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1505586008000",
            "Effect": "Allow",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::<ses-bucket-name>/*"
            ]
        }
    ]
}

** Politique; kms-decrypt-zaru ** Pour «», spécifiez l'ARN de la clé de chiffrement.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1448696327000",
            "Effect": "Allow",
            "Action": [
                "kms:Decrypt"
            ],
            "Resource": [
                "<kms-arn>"
            ]
        }
    ]
}

Enfin, attachez ces deux stratégies au rôle d'exécution de la fonction Lambda. Tout d'abord, allez dans «Rôle» dans la console IAM, sélectionnez un rôle et attachez-le à partir de «Attach Policy». lambda_08.png

Contrôle de fonctionnement

Cela devrait maintenant fonctionner. Veuillez définir l'adresse e-mail définie pour SES dans «À» et l'adresse e-mail de l'autre partie dans «Répondre à», et envoyez-la avec un fichier approprié en pièce jointe. Comment c'est?

Résumé

Zip Dontokoi attaché!

Recommended Posts

J'ai réussi à le faire parce que la coutume de joindre un zip avec un mot de passe à un e-mail et de dire «Je vous enverrai le mot de passe séparément» est gênante.
Si vous les gars dans la cuisine de portée pouvez le faire avec une marge ~ ♪
Fabriquez-vous quelque chose comme une fusée?
Lors de l'écriture d'un test en utilisant DB avec django, il peut être plus rapide d'utiliser `setUpTestData ()`
J'ai réussi à le faire parce que la coutume de joindre un zip avec un mot de passe à un e-mail et de dire «Je vous enverrai le mot de passe séparément» est gênante.
C'est une histoire de ferroutage sur le service qui renvoie "Nyan" lorsque vous appuyez sur ping
C'était un peu difficile de faire flacon avec la version docker de nginx-unit
Jusqu'à ce que vous puissiez installer Blender et l'exécuter avec python pour le moment
Le son émis par M. Tick sur le lieu de travail est ... J'ai réussi à le faire avec le code
Il est étonnamment difficile d'obtenir une liste de la dernière date et heure de connexion des espaces de travail
Exportez le rapport au format PDF à partir de DB avec Python et attachez-le automatiquement à un e-mail et envoyez-le
Que faire si vous ne parvenez pas à envoyer un e-mail à Yahoo avec Python.
Je souhaite envoyer Gmail avec Python, mais je ne peux pas en raison d'une erreur
Je veux écrire un élément dans un fichier avec numpy et le vérifier.
C'était un peu difficile de faire flacon avec la version docker de nginx-unit
Je n'aime pas être frustré par la sortie de Pokemon Go, j'ai donc créé un script pour détecter la sortie et le tweeter
Un script qui envoie un ping au serveur enregistré et envoie un e-mail avec Gmail un certain nombre de fois en cas d'échec
J'ai réfléchi un peu car Trace Plot du paramètre de stan est difficile à voir
Notez ce que vous voulez faire à l'avenir avec Razpai
Que faire si vous chattez ou suivez un fichier binaire et que le terminal est brouillé
[Python] Qu'est-ce qu'une tranche? Une explication facile à comprendre de son utilisation avec un exemple concret
J'ai essayé de rendre possible l'envoi automatique d'un e-mail en double-cliquant simplement sur l'icône [Python]
Que faire lorsqu'une partie de l'image d'arrière-plan devient transparente lorsque l'image transparente est combinée avec Oreiller
Il est difficile de changer les paramètres entre l'intranet et le voyage d'affaires / à la maison, donc j'étais un peu heureux lorsque j'ai mis en place un proxy de transfert localement avec Apache2.
Quand il est difficile de copier ce que vous avez construit avec vue
J'ai créé un script POST pour créer un problème sur Github et l'enregistrer dans le projet
Est-il possible de se lancer dans une entreprise de pré-cotation et de faire fortune avec des stock-options?
[AWS lambda] Déployer, y compris diverses bibliothèques avec lambda (générer un zip avec un mot de passe et le télécharger vers s3) @ Python
Comme c'est le 20e anniversaire de la formation, j'ai essayé de visualiser les paroles de Parfum avec Word Cloud
J'ai essayé de rendre possible l'envoi automatique d'un e-mail en double-cliquant simplement sur l'icône [GAS / Python]
L'histoire de la création d'un outil pour charger une image avec Python ⇒ l'enregistrer sous un autre nom
J'ai fait un package npm pour obtenir l'ID de la carte IC avec Raspberry Pi et PaSoRi
Lisez les données du lecteur NFC connecté à Raspberry Pi 3 avec Python et envoyez-les à openFrameworks avec OSC
Mettez à jour les données en les téléchargeant sur s3 d'aws avec une commande, et supprimez les données utilisées (en chemin)