├── README.md ├── api_client └── __init__.py ├── email2pb.py ├── pushbullet.py └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | Email 2 PushBullet 2 | ======== 3 | 4 | Email to PushBullet notification 5 | 6 | 7 | This simple script allows to redirect mail input(from postfix, for example) from a certain mail address to PushBullet notification. Useful to send pushes from sources which are able to send email only. 8 | 9 | 10 | ### Example usage 11 | 12 | Lets imagine that we want to redirect all emails sent to push@example.com to your PushBullet account(and therefore to your mobile devices, browser excensions, etc) 13 | Keep in mind that instructions below were tested on Debian 6 with python 2.6 14 | So, thats what you should do: 15 | 16 | #### Step zero: setup and configure postfix for domain example.com and other prerequisites 17 | 18 | Bla-bla-bla 19 | 20 | #### Step 1: create shell script 21 | 22 | First, create shell script which will contain email2pb call and an API key. If you will directly specify python script with a API key in aliases file - this'll be a major security hole. 23 | So our script will be something like this: 24 | 25 | ``` 26 | #!/bin/sh 27 | /usr/bin/python /var/spool/postfix/email2pb/email2pb.py --key YOUR_PUSHBULLET_API_KEY 28 | ``` 29 | 30 | Let's name it...umm... `/var/spool/postfix/email2pb/email2pb` 31 | And make it executable: 32 | 33 | ``` 34 | chmod +x /var/spool/postfix/email2pb/email2pb 35 | ``` 36 | 37 | Why there? My example was tested in Debian, and postfix's home dir on Debian 6 is /var/spool/postfix 38 | Rememer, postfix should be able to acces your script. 39 | 40 | 41 | #### Step 2: add mail alias 42 | 43 | Open /etc/aliases file and append a line there: 44 | 45 | ``` 46 | push: |/var/spool/postfix/email2pb/email2pb 47 | ``` 48 | Save the file and execute `newaliases` command. 49 | 50 | #### Step 3: test it 51 | 52 | Send email to push@example.com and, if it didn't work, check /var/log/mail.log 53 | 54 | 55 | Thats it. 56 | -------------------------------------------------------------------------------- /api_client/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from urllib.parse import urlsplit, urlunsplit 4 | 5 | import requests 6 | 7 | 8 | class BaseAPIClient(object): 9 | """ Base class for API clients implementation 10 | """ 11 | force_trailing_backslash = True 12 | 13 | def __init__(self): 14 | self.headers = { 15 | "Content-Type": "application/json", 16 | } 17 | self.auth = None 18 | 19 | 20 | def prepare_url(self, path): 21 | parts = urlsplit(self.get_endpoint()) 22 | 23 | if self.force_trailing_backslash and path[-1] != "/": 24 | path += "/" 25 | 26 | return urlunsplit(( 27 | parts.scheme, parts.netloc, 28 | os.path.join(parts.path, path), 29 | "", "")) 30 | 31 | 32 | def _make_request(self, url, method, headers, data, **kwargs): 33 | auth = self.auth or None 34 | 35 | if method == "POST": 36 | response = requests.post( 37 | url, data=data, headers=headers, auth=auth, **kwargs) 38 | elif method == "PUT": 39 | response = requests.put( 40 | url, data=data, headers=headers, auth=auth, **kwargs) 41 | elif method == "DELETE": 42 | response = requests.delete( 43 | url, data=data, headers=headers, auth=auth, **kwargs) 44 | else: 45 | response = requests.get( 46 | url, headers=headers, params=data, auth=auth, **kwargs) 47 | 48 | return response 49 | 50 | 51 | def _make_requests( 52 | self, path, method="GET", data=None, extra_headers=None, 53 | retry_count=1, **kwargs): 54 | 55 | headers = self.prepare_headers(extra_headers=extra_headers) 56 | data = self.prepare_data(data, method) 57 | 58 | url = self.prepare_url(path) 59 | 60 | try_count = 1 + (0 if not retry_count else retry_count) 61 | 62 | while try_count > 0: 63 | try_count -= 1 64 | response = self._make_request( 65 | url, method, headers, data, **kwargs) 66 | 67 | if not self.should_retry(response, try_count): 68 | break 69 | 70 | return self.process_response(response) 71 | 72 | 73 | def prepare_headers(self, extra_headers=None): 74 | headers = {} 75 | headers.update(self.headers) 76 | if extra_headers: 77 | headers.update(extra_headers) 78 | 79 | return headers 80 | 81 | 82 | def prepare_data(self, data, method="POST"): 83 | if data is not None: 84 | if method in ["POST", "PUT"]: 85 | return json.dumps(data) 86 | 87 | return data 88 | 89 | 90 | def should_retry(self, response, retries_left): 91 | return False 92 | 93 | 94 | def get_endpoint(self): 95 | """ 96 | Returns API endpoint, a full URL 97 | """ 98 | raise NotImplementedError( 99 | "You must create a subclass of APIClient to use it") 100 | 101 | 102 | def post( 103 | self, url, data=None, extra_headers=None, retry_count=1, 104 | **kwargs): 105 | response = self._make_requests( 106 | url, method='POST', data=data, extra_headers=extra_headers, 107 | retry_count=retry_count, **kwargs) 108 | return response 109 | 110 | 111 | def put( 112 | self, url, data=None, extra_headers=None, retry_count=1, 113 | **kwargs): 114 | response = self._make_requests( 115 | url, method='PUT', data=data, extra_headers=extra_headers, 116 | retry_count=retry_count, **kwargs) 117 | return response 118 | 119 | 120 | def get( 121 | self, url, data=None, extra_headers=None, retry_count=1, 122 | **kwargs): 123 | response = self._make_requests( 124 | url, method='GET', data=data, extra_headers=extra_headers, 125 | retry_count=retry_count, **kwargs) 126 | return response 127 | 128 | 129 | def delete( 130 | self, url, data=None, extra_headers=None, retry_count=1, 131 | **kwargs): 132 | response = self._make_requests( 133 | url, method='DELETE', data=data, extra_headers=extra_headers, 134 | retry_count=retry_count, **kwargs) 135 | return response 136 | 137 | 138 | def process_response(self, response): 139 | response.raise_for_status() 140 | 141 | return response 142 | -------------------------------------------------------------------------------- /email2pb.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import base64 3 | import email 4 | import logging 5 | import re 6 | import sys 7 | 8 | from pushbullet import PushbulletAPIClient 9 | 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | parser = argparse.ArgumentParser(description='Send PushBullet PUSH based on email message') 15 | parser.add_argument( 16 | 'infile', nargs='?', type=argparse.FileType('r'), default=sys.stdin, 17 | help='MIME-encoded email file(if empty, stdin will be used)') 18 | parser.add_argument('--key', help='API key for PushBullet', required=True) 19 | parser.add_argument('--debug', help='Enable debug mode', action='store_true') 20 | parser.add_argument("--debug_log", type=str) 21 | args = parser.parse_args() 22 | 23 | stdin_data = args.infile.read() 24 | 25 | 26 | debug_mode = args.debug 27 | if debug_mode: 28 | logger.debug('Debug mode enabled') 29 | logfile_path = args.debug_log or "debug.log" 30 | with open(logfile_path, "w") as debug_log: 31 | debug_log.write("\n") 32 | debug_log.write("Incoming message:\n") 33 | debug_log.write("-------------------------\n") 34 | debug_log.write(stdin_data) 35 | debug_log.write("-------------------------\n") 36 | 37 | msg = email.message_from_string(stdin_data) 38 | args.infile.close() 39 | 40 | def decode_field(field_raw): 41 | match = re.match(r'\=\?([^\?]+)\?([BQ])\?([^\?]+)\?\=', field_raw) 42 | if match: 43 | charset, encoding, field_coded = match.groups() 44 | if encoding == 'B': 45 | field_coded = bytearray(field_coded, encoding=charset) 46 | field_coded = base64.decodestring(field_coded) 47 | return field_coded.decode(charset) 48 | else: 49 | return field_raw 50 | 51 | subject_raw = msg.get('Subject', '') 52 | subject = decode_field(subject_raw) 53 | 54 | sender = decode_field(msg.get('From', '')) 55 | 56 | body_text = '' 57 | for part in msg.walk(): 58 | if part.get_content_type() == 'text/plain': 59 | body_part = part.get_payload() 60 | part_encoding = part.get_content_charset() 61 | 62 | if part.get('Content-Transfer-Encoding') == 'base64': 63 | body_part = bytearray(body_part, encoding=part_encoding) 64 | body_part = base64.decodestring(body_part) 65 | body_part = body_part.decode(part_encoding) 66 | 67 | if body_text: 68 | body_text = '%s\n%s' % (body_text, body_part) 69 | else: 70 | body_text = body_part 71 | 72 | body_text = '%s\nFrom: %s' % (body_text, sender) 73 | 74 | client = PushbulletAPIClient(api_key=args.key) 75 | client.push_note_to_device(None, body_text, title=subject) 76 | -------------------------------------------------------------------------------- /pushbullet.py: -------------------------------------------------------------------------------- 1 | from api_client import BaseAPIClient 2 | 3 | 4 | class PushbulletAPIClient(BaseAPIClient): 5 | force_trailing_backslash = False 6 | 7 | 8 | def __init__(self, api_key=None, *args, **kwargs): 9 | super(PushbulletAPIClient, self).__init__(*args, **kwargs) 10 | if api_key: 11 | self.api_key = api_key 12 | else: 13 | try: 14 | with open("api_key.txt", "r") as key_file: 15 | self.api_key = key_file.read().strip() 16 | except: 17 | raise Exception( 18 | "API key wasn't specified nor in start parameters, " 19 | "neither in api_key.txt file") 20 | 21 | 22 | def get_endpoint(self): 23 | return "https://api.pushbullet.com/v2/" 24 | 25 | 26 | def prepare_headers(self, extra_headers=None): 27 | headers = super(PushbulletAPIClient, self).prepare_headers( 28 | extra_headers=extra_headers) 29 | 30 | headers["Access-Token"] = self.api_key 31 | 32 | return headers 33 | 34 | def get_devices(self): 35 | response = self.get("devices") 36 | return response.json() 37 | 38 | 39 | def push_to_device(self, device_id, push_data): 40 | data = { 41 | "device_iden": device_id, 42 | } 43 | 44 | data.update(push_data) 45 | 46 | response = self.post("pushes", data=data) 47 | return response.json() 48 | 49 | def push_note_to_device(self, device_id, body, title=""): 50 | note = { 51 | "type": "note", 52 | "title": title, 53 | "body": body, 54 | } 55 | result = self.push_to_device(device_id, note) 56 | return result 57 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.22.0 2 | --------------------------------------------------------------------------------