├── .dockerignore ├── .drone.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── etc └── slacker │ └── config.dist.yml ├── handler.py ├── requirements.txt └── test.txt /.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | venv/ 3 | etc/ 4 | -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | # vim: ts=2:sts=2:sw=2:expandtab:smarttab: 2 | 3 | pipeline: 4 | publish-docker: 5 | image: plugins/docker 6 | tag: 7 | - latest 8 | - ${DRONE_TAG} 9 | repo: ontrif/slacker 10 | when: 11 | event: tag 12 | secrets: [ docker_username, docker_password ] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # Drone 92 | .drone.sec.yml 93 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:alpine3.18 2 | WORKDIR /app 3 | ENV PYTHONPATH /app 4 | ADD ./ /app 5 | RUN mkdir -p /etc/slacker 6 | RUN apk add --no-cache git && pip install -r requirements.txt 7 | CMD ["aiosmtpd", "-n", "-l", "0.0.0.0:8025", "-c", "handler.MessageHandler"] 8 | EXPOSE 8025 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 ont 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slacker 2 | 3 | Slacker is simple email-to-slack gateway. It support basic rules for routing 4 | messages to different slack channels from different bots. 5 | 6 | Main purpose of slacker is to redirect messages from old-school systems which doesn't 7 | support messaging other than email. For example cron and monit 8 | (https://mmonit.com/monit/) support only emails as the only system for 9 | notifications. 10 | 11 | ## Installation 12 | 13 | Slacker can be easily installed via docker: 14 | 15 | ```bash 16 | docker pull ontrif/slacker 17 | ``` 18 | 19 | Then run container with custom slacker `config.yml`: 20 | 21 | ```bash 22 | docker run \ 23 | -d --restart=always \ 24 | --name=slacker \ 25 | -v /path/to/config.yml:/etc/slacker/config.yml \ 26 | -p localhost:8025:8025 \ 27 | ontrif/slacker 28 | ``` 29 | Last command will start SMTP server on `localhost:8025` 30 | 31 | ## Config 32 | 33 | Slacker supports simple list of rules for configuring target slack channel, bot 34 | name and its avatar depending on email content. 35 | 36 | There are two sections: 37 | * default: this is default options for sending message to slack. 38 | * rules: list of rules for matching email message against 'from', 'to' and/or 'subject'. 39 | 40 | Each rule in list tested in order. First matched rule is used to update 41 | options values from 'default' section of config. 42 | 43 | Example `config.yml` for redirecting email to two channels: `#monit` and `#cron`: 44 | ```yaml 45 | # default values for channel, bot name, avatar url, slack token and debug mode 46 | default: 47 | channel: "#general" 48 | username: "slacker" 49 | icon_url: "" 50 | slack_token: "xoxb-00000000000-aaaaaaaaaaaaaaaaaaaaaaaa" 51 | debug: false 52 | 53 | 54 | # list of rules 55 | rules: 56 | - name: "Monit rule" 57 | from: "monit@.*" # all emails from monit@localhost will match this rule 58 | 59 | options: 60 | username: "monit" 61 | channel: "#monit" 62 | icon_url: "https://bitbucket.org/tildeslash/monit/avatar/128" 63 | debug: false 64 | 65 | 66 | - name: "Cron rule" 67 | from: "root@localhost" 68 | subject: "Cron.*" # cron email subject starts with "Cron..." 69 | 70 | options: 71 | username: "cron" 72 | channel: "#cron" 73 | icon_url: "" 74 | debug: true # will output full email with all X-headers 75 | ``` 76 | -------------------------------------------------------------------------------- /etc/slacker/config.dist.yml: -------------------------------------------------------------------------------- 1 | ###### 2 | # This is example of config file for slacker. 3 | # There are two sections: 4 | # * default: this is default options for sending message to slack. 5 | # * rules: list of rules for matching email message. Currently 'from', 6 | # 'to' and 'subject' fields are supported. Their values can be 7 | # regexp to match corresponding email field. 8 | # 9 | # Each rule in list tested in order. First matched rule is used to update 10 | # values from 'default' section with its 'options' subsection. 11 | ###### 12 | 13 | default: 14 | channel: '#general' 15 | username: slacker 16 | icon_url: '' 17 | slack_token: xoxb-00000000000-aaaaaaaaaaaaaaaaaaaaaaaa 18 | debug: false 19 | format: "subject: %(subject)s; body: %(body)s" ## default slack message format 20 | 21 | 22 | rules: 23 | - name: Monit rule 24 | from: monit@.* 25 | 26 | options: 27 | username: monit 28 | channel: '#monit' 29 | icon_url: 'https://bitbucket.org/tildeslash/monit/avatar/128' 30 | debug: false 31 | 32 | 33 | - name: Cron rule 34 | from: root@localhost 35 | subject: Cron.* 36 | 37 | options: 38 | username: cron 39 | channel: '#cron' 40 | icon_url: '' 41 | debug: true ## will output full email with all headers 42 | format: "email body is: %(body)s" ## custom message format 43 | -------------------------------------------------------------------------------- /handler.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import yaml 4 | import slack 5 | import slack.chat 6 | from aiosmtpd.handlers import Message 7 | 8 | 9 | class MessageHandler(Message): 10 | def __init__(self, *args, **kargs): 11 | Message.__init__(self, *args, **kargs) 12 | 13 | config = os.getenv('CONFIG', '/etc/slacker/config.yml') 14 | print(config) 15 | if not os.path.exists(config): 16 | print('Config doesn\'t exists!') 17 | exit(1) 18 | 19 | self.config = yaml.safe_load(open(config)) 20 | 21 | def handle_message(self, message): 22 | """ This method will be called by aiosmtpd server when new mail will 23 | arrived. 24 | """ 25 | options = self.process_rules(message) 26 | 27 | print('matched', options) 28 | self.send_to_slack(self.extract_text(message), **options) 29 | 30 | if options['debug']: 31 | self.send_to_slack('DEBUG: ' + str(message), **options) 32 | 33 | def process_rules(self, message): 34 | """ Check every rule from config and returns options from matched 35 | """ 36 | default = self.config['default'] 37 | 38 | fields = { 39 | 'from': message['From'], 40 | 'to': message['To'], 41 | 'subject': message['Subject'], 42 | 'body': message.get_payload() 43 | } 44 | 45 | print(fields) 46 | 47 | for rule in self.config['rules']: 48 | # TODO: better handling of None values than just str(value) 49 | tests = ( 50 | re.match(rule[field], str(value)) 51 | for field, value in fields.items() if field in rule 52 | ) 53 | 54 | if all(tests): 55 | options = default.copy() 56 | options.update(rule['options']) 57 | return options 58 | 59 | return default 60 | 61 | def extract_text(self, message): 62 | fmt = self.config['default'].get('format', '%(body)s') 63 | body = message.get_payload() 64 | subject = message['Subject'] 65 | return fmt % dict(body=body, subject=subject) 66 | 67 | def send_to_slack(self, text, **options): 68 | print('sending to slack', text, options) 69 | 70 | slack.api_token = options['slack_token'] 71 | slack.chat.post_message( 72 | options['channel'], 73 | text, 74 | username=options['username'], 75 | icon_url=options['icon_url'] 76 | ) 77 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiosmtpd==1.4.5 2 | atpublic==4.0 3 | attrs==23.1.0 4 | certifi==2023.7.22 5 | charset-normalizer==3.2.0 6 | idna==3.7 7 | pyslack==0.5.0 8 | PyYAML==6.0.1 9 | requests==2.32.0 10 | urllib3==2.0.7 11 | -------------------------------------------------------------------------------- /test.txt: -------------------------------------------------------------------------------- 1 | 123 2 | --------------------------------------------------------------------------------