├── setup.cfg ├── MANIFEST.in ├── github_email_explorer ├── __init__.py ├── api_url.py ├── cli_explore.py ├── cli_sendgrid.py ├── email_handler.py └── github_email.py ├── .gitignore ├── examples ├── oauth_github.png ├── marketing_email.png ├── marketing_email.txt └── get_email.py ├── requirements.txt ├── .travis.yml ├── .codeclimate.yml ├── setup.py └── README.md /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file=README.md -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include requirements.txt 3 | -------------------------------------------------------------------------------- /github_email_explorer/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.2.8' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | venv 3 | *.pyc 4 | .idea 5 | .ropeproject 6 | *.log 7 | *.egg-info 8 | dist 9 | -------------------------------------------------------------------------------- /examples/oauth_github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuecen/github-email-explorer/HEAD/examples/oauth_github.png -------------------------------------------------------------------------------- /examples/marketing_email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuecen/github-email-explorer/HEAD/examples/marketing_email.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Jinja2==2.10.1 2 | MarkupSafe==0.23 3 | python-http-client==2.1.1 4 | requests==2.20.0 5 | sendgrid==3.1.10 6 | tabulate==0.7.5 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | # command to install dependencies 5 | install: "pip install -r requirements.txt" 6 | # command to run tests 7 | script: nosetests 8 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | radon: 3 | enabled: true 4 | config: 5 | python_version: 2 6 | 7 | # required for grades to appear in web UI 8 | ratings: 9 | paths: 10 | - "**.py" 11 | -------------------------------------------------------------------------------- /examples/marketing_email.txt: -------------------------------------------------------------------------------- 1 | subject: Thanks for trying {{repository}} 2 | from: test@example.com 3 | user: 4 | repository: yuecen/github-email-explorer 5 | repository_owner: yuecen 6 | repository_name: github-email-explorer 7 | site: GitHub 8 | 9 |

Hi {{user.name}} ({{user.g_id}}),

10 |

Thank you for trying {{repository_owner}}/{{repository_name}}!

11 | 12 |

...

13 | 14 |

I look forward to seeing you on GitHub :)

15 |

yuecen

16 | -------------------------------------------------------------------------------- /examples/get_email.py: -------------------------------------------------------------------------------- 1 | from github_email_explorer import github_email 2 | 3 | ges = github_email.collect_email_info('yuecen', 'github-email-explorer', ['star', 'watch']) 4 | 5 | for ge in ges: 6 | print ge.g_id, "->", ge.name, ",", ge.email 7 | 8 | # With Authentication 9 | # github_api_auth = ('', '') 10 | # ges = github_email.collect_email_info('yuecen', 'github-email-explorer', ['star', 'watch'], 11 | # github_api_auth=github_api_auth) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | from setuptools import setup, find_packages 4 | 5 | pkg_file = open("github_email_explorer/__init__.py").read() 6 | metadata = dict(re.findall("__([a-z]+)__\s*=\s*'([^']+)'", pkg_file)) 7 | description = open('README.md').read() 8 | requirements = [i.strip() for i in open("requirements.txt").readlines()] 9 | 10 | setup( 11 | name='github-email-explorer', 12 | description='A tool to get email addresses by action types such as starred, watching or fork on GitHub repositories; Send email content to those addresses with a template.', 13 | version=metadata['version'], 14 | 15 | # Author details 16 | author='yuecen', 17 | author_email='yuecendev+pypi@gmail.com', 18 | url='https://github.com/yuecen/github-email-explorer', 19 | long_description=description, 20 | license='MIT', 21 | classifiers=[ 22 | 'Intended Audience :: Developers', 23 | 'Programming Language :: Python', 24 | 'Topic :: Communications :: Email', 25 | 'Topic :: Software Development :: Libraries :: Python Modules', 26 | ], 27 | keywords='github, email, sendgrid, marketing', 28 | install_requires=requirements, 29 | packages=find_packages(), 30 | entry_points={ 31 | 'console_scripts': [ 32 | 'ge-explore = github_email_explorer.cli_explore:get_github_email_by_repo', 33 | 'ge-sendgrid = github_email_explorer.cli_sendgrid:send_email_by_sendgrid' 34 | ] 35 | } 36 | ) -------------------------------------------------------------------------------- /github_email_explorer/api_url.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from urllib import urlencode 3 | from urlparse import parse_qs, urlsplit, urlunsplit 4 | 5 | 6 | class GitHubEndPoint(object): 7 | api_url = 'https://api.github.com/' 8 | 9 | @staticmethod 10 | def user_profile(user_id): 11 | return '{}users/{}'.format(GitHubEndPoint.api_url, user_id) 12 | 13 | @staticmethod 14 | def user_events(user_id): 15 | return '{}users/{}/events/public'.format(GitHubEndPoint.api_url, user_id) 16 | 17 | @staticmethod 18 | def repository(user_id, repo): 19 | return '{}repos/{}/{}'.format(GitHubEndPoint.api_url, user_id, repo) 20 | 21 | @staticmethod 22 | def stargazers(user_id, repo): 23 | return '{}repos/{}/{}/stargazers'.format(GitHubEndPoint.api_url, user_id, repo) 24 | 25 | @staticmethod 26 | def forks(user_id, repo): 27 | return '{}repos/{}/{}/forks'.format(GitHubEndPoint.api_url, user_id, repo) 28 | 29 | @staticmethod 30 | def watchers(user_id, repo): 31 | return '{}repos/{}/{}/subscribers'.format(GitHubEndPoint.api_url, user_id, repo) 32 | 33 | @staticmethod 34 | def rate_limit(): 35 | return '{}rate_limit'.format(GitHubEndPoint.api_url) 36 | 37 | @staticmethod 38 | def add_auth_info(url, github_api_auth): 39 | if github_api_auth is None: 40 | github_api_auth = ('', '') 41 | url = set_url_parameter(url, 'client_id', github_api_auth[0]) 42 | url = set_url_parameter(url, 'client_secret', github_api_auth[1]) 43 | return url 44 | 45 | @staticmethod 46 | def pagination(url, page=1, per_page=100): 47 | url = set_url_parameter(url, 'page', page) 48 | url = set_url_parameter(url, 'per_page', per_page) 49 | return url 50 | 51 | 52 | def set_url_parameter(url, param_name, param_value): 53 | """Set or replace a query parameter and return the modified URL. 54 | 55 | >>> set_url_parameter('http://example.com?foo=bar&biz=baz', 'foo', 'stuff') 56 | 'http://example.com?foo=stuff&biz=baz' 57 | 58 | """ 59 | scheme, netloc, path, query_string, fragment = urlsplit(url) 60 | query_params = parse_qs(query_string) 61 | 62 | query_params[param_name] = [param_value] 63 | new_query_string = urlencode(query_params, doseq=True) 64 | 65 | return urlunsplit((scheme, netloc, path, new_query_string, fragment)) -------------------------------------------------------------------------------- /github_email_explorer/cli_explore.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from collections import namedtuple 3 | from datetime import datetime 4 | from tabulate import tabulate 5 | import argparse 6 | import re 7 | 8 | import github_email 9 | 10 | 11 | def record_args(name, d): 12 | return namedtuple(name, d.keys())(**d) 13 | 14 | 15 | class ExploreCliArgs(object): 16 | def __init__(self): 17 | p = argparse.ArgumentParser(prog='ge-explore') 18 | p.add_argument('--repo', help='Repo on Github, type "/"') 19 | p.add_argument('--action_type', default=['star'], nargs='+', help='"star", "fork" and "watch" are the only three options now') 20 | p.add_argument('--client_id', help='Github OAuth client ID') 21 | p.add_argument('--client_secret', help='Github OAuth client secret') 22 | p.add_argument('--status', action='store_true', help='Github API status') 23 | 24 | args = p.parse_args() 25 | 26 | self.repo = args.repo 27 | if self.repo: 28 | tmp = re.split('/', self.repo) 29 | assert len(tmp) == 2, "repo format is not correct" 30 | 31 | self.repo = record_args('repo', {'owner': tmp[0], 'name': tmp[1]}) 32 | 33 | self.action_type = args.action_type 34 | self.client_id = args.client_id if args.client_id else '' 35 | self.client_secret = args.client_secret if args.client_secret else '' 36 | self.status = args.status 37 | 38 | 39 | def get_github_email_by_repo(): 40 | """ Get user email by repos 41 | """ 42 | explore_cli_args = ExploreCliArgs() 43 | 44 | github_api_auth = (explore_cli_args.client_id, explore_cli_args.client_secret) 45 | 46 | if explore_cli_args.status: 47 | # call api status 48 | status = github_email.api_status(github_api_auth) 49 | table = [["Core", status.core_limit, status.core_remaining, datetime.utcfromtimestamp(status.core_reset_time).strftime('%Y-%m-%dT%H:%M:%SZ')], 50 | ["Search", status.search_limit, status.search_remaining, datetime.utcfromtimestamp(status.search_reset_time).strftime('%Y-%m-%dT%H:%M:%SZ')]] 51 | print "== GitHub API Status ==" 52 | print tabulate(table, headers=['Resource Type', 'Limit', 'Remaining', 'Reset Time']) 53 | return 54 | 55 | # handle action_type 56 | ges = github_email.collect_email_info(explore_cli_args.repo.owner, explore_cli_args.repo.name, explore_cli_args.action_type, github_api_auth) 57 | print 'Total: {}/{}'.format(len([ge for ge in ges if ge.email]), len(ges)) 58 | print github_email.format_email(ges) 59 | 60 | 61 | if __name__ == '__main__': 62 | get_github_email_by_repo() 63 | -------------------------------------------------------------------------------- /github_email_explorer/cli_sendgrid.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import argparse 4 | 5 | import github_email 6 | from email_handler import get_email_template 7 | from email_handler import parse_into_github_user_emails 8 | from email_handler import send_sendgrid_by_ges 9 | 10 | 11 | class SendGridCliArgs(object): 12 | def __init__(self): 13 | p = argparse.ArgumentParser(prog='ge-sendgrid') 14 | p.add_argument('--api_key', help='Your KEY of SendGrid API') 15 | p.add_argument('--template_path', help='Your email template') 16 | p.add_argument('--action_type', default=['star'], nargs='+', help='"star", "fork" and "watch" are the only three options now') 17 | p.add_argument('--client_id', help='Github OAuth client ID') 18 | p.add_argument('--client_secret', help='Github OAuth client secret') 19 | 20 | args = p.parse_args() 21 | 22 | self.api_key = args.api_key 23 | self.action_type = args.action_type 24 | self.template_path = args.template_path 25 | self.client_id = args.client_id if args.client_id else '' 26 | self.client_secret = args.client_secret if args.client_secret else '' 27 | 28 | 29 | def send_email_by_sendgrid(): 30 | """ 31 | Send email via SendGrid 32 | """ 33 | sendgrid_cli_args = SendGridCliArgs() 34 | 35 | github_email_template = get_email_template(sendgrid_cli_args.template_path) 36 | metadata = github_email_template.metadata 37 | # List first 38 | if metadata['user']: 39 | print "Send email by list..." 40 | ges = parse_into_github_user_emails(metadata['user']) 41 | print 'Total email addresses from list: {}'.format(len(ges)) 42 | send_sendgrid_by_ges(github_user_emails=ges, 43 | sendgrid_api_key=sendgrid_cli_args.api_key, 44 | github_email_template=github_email_template) 45 | 46 | else: 47 | print "Send email by exploring..." 48 | # explore users email by action types 49 | github_api_auth = (sendgrid_cli_args.client_id, sendgrid_cli_args.client_secret) 50 | ges = github_email.collect_email_info(metadata['repository_owner'], metadata['repository_name'], sendgrid_cli_args.action_type, github_api_auth) 51 | print 'Total: {}/{}'.format(len([ge for ge in ges if ge.email]), len(ges)) 52 | 53 | # send email by py-sendgrid 54 | send_sendgrid_by_ges(github_user_emails=ges, 55 | sendgrid_api_key=sendgrid_cli_args.api_key, 56 | github_email_template=github_email_template) 57 | 58 | 59 | if __name__ == '__main__': 60 | send_email_by_sendgrid() 61 | -------------------------------------------------------------------------------- /github_email_explorer/email_handler.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from github_email import GithubUserEmail 3 | 4 | from jinja2 import FileSystemLoader 5 | from jinja2 import Environment 6 | 7 | from sendgrid import SendGridAPIClient 8 | from sendgrid.helpers.mail import Email, Content, Mail 9 | 10 | import codecs 11 | import re 12 | import sys 13 | 14 | 15 | template_loader = FileSystemLoader(searchpath="/") 16 | template_env = Environment(loader=template_loader) 17 | 18 | ESSENTIAL_FIELDS = ['subject', 'from', 'from_name', 'user', 19 | 'repository', 'repository_owner', 'repository_name'] 20 | 21 | 22 | def parse_email(address): 23 | r = re.match('(?P.*)\s*\((?P.*)\)\s*<(?P.*?)>', address) 24 | if r: 25 | e = r.groupdict() 26 | name, g_id, email = e['name'].strip(), e['g_id'].strip(), e['email'].strip() 27 | else: 28 | name, g_id, email = '', '', address.strip() 29 | 30 | return name, g_id, email 31 | 32 | def parse_into_github_user_emails(email_list): 33 | email_list = re.split(';', email_list) 34 | github_user_emails = [GithubUserEmail(parse_email(address)) for address in email_list if len(address.strip()) > 0] 35 | return github_user_emails 36 | 37 | 38 | def send_sendgrid_by_ges(github_user_emails=None, sendgrid_api_key=None, github_email_template=None): 39 | github_user_emails = [ge for ge in github_user_emails if ge.email] 40 | 41 | send_sendgrid(github_user_emails=github_user_emails, sendgrid_api_key=sendgrid_api_key, 42 | github_email_template=github_email_template) 43 | 44 | 45 | def send_sendgrid(sendgrid_api_key=None, github_email_template=None, github_user_emails=None): 46 | 47 | assert sendgrid_api_key, "SendGrid API key is required" 48 | 49 | sg = SendGridAPIClient(apikey=sendgrid_api_key) 50 | 51 | metadata = github_email_template.metadata 52 | 53 | from_email = Email(metadata['from']) 54 | from_email.name = metadata['from_name'] 55 | for ge in github_user_emails: 56 | 57 | # Add github_user into metadata 58 | metadata['user'] = ge 59 | 60 | # Render content with metadata 61 | content = Content("text/html", github_email_template.render_content(metadata)) 62 | 63 | # Render subject with metadata 64 | subject = template_env.from_string(metadata['subject']).render(metadata) 65 | 66 | to_email = Email(ge.email) 67 | mail = Mail(from_email, subject, to_email, content) 68 | _body = mail.get() 69 | 70 | # Add custom args for log fields 71 | _custon = {} 72 | for key, value in metadata.iteritems(): 73 | if key not in ESSENTIAL_FIELDS: 74 | _custon[key] = value 75 | _custon['repository'] = metadata['repository'] 76 | _body['custom_args'] = _custon 77 | 78 | response = sg.client.mail.send.post(request_body=_body) 79 | 80 | if response.status_code > 300: 81 | # More than 300 means something wrong, do nothing. 82 | sys.exit() 83 | 84 | 85 | def get_email_template(path): 86 | github_email_template = GitHubEmailTemplate() 87 | github_email_template.set_material(path) 88 | return github_email_template 89 | 90 | 91 | class BaseReader(object): 92 | def __init__(self, *args, **kwargs): 93 | pass 94 | 95 | def read(self, source_path): 96 | """Return metadata and template""" 97 | material = material_open(source_path) 98 | res = re.match(r'(?P[\s\S]*?)\n\n(?P