├── .gitignore ├── 00_markdown_merge.ipynb ├── LICENSE ├── Makefile ├── README.md ├── docs ├── .gitignore ├── 00* ├── Gemfile ├── _config.yml ├── _data │ ├── alerts.yml │ ├── definitions.yml │ ├── glossary.yml │ ├── sidebars │ │ └── home_sidebar.yml │ ├── tags.yml │ ├── terms.yml │ └── topnav.yml ├── _includes │ ├── archive.html │ ├── callout.html │ ├── footer.html │ ├── google_analytics.html │ ├── head.html │ ├── head_print.html │ ├── image.html │ ├── important.html │ ├── initialize_shuffle.html │ ├── inline_image.html │ ├── links.html │ ├── note.html │ ├── search_google_custom.html │ ├── search_simple_jekyll.html │ ├── sidebar.html │ ├── tip.html │ ├── toc.html │ ├── topnav.html │ └── warning.html ├── _layouts │ ├── default.html │ ├── default_print.html │ ├── none.html │ ├── page.html │ └── page_print.html ├── css │ ├── bootstrap.min.css │ ├── boxshadowproperties.css │ ├── customstyles.css │ ├── font-awesome.min.css │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ ├── modern-business.css │ ├── printstyles.css │ ├── syntax.css │ ├── theme-blue.css │ └── theme-green.css ├── feed.xml ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── images │ ├── company_logo.png │ ├── company_logo_big.png │ ├── favicon.ico │ └── workflowarrow.png ├── index.html ├── js │ ├── customscripts.js │ ├── jekyll-search.js │ ├── jquery.ba-throttle-debounce.min.js │ ├── jquery.navgoco.min.js │ ├── jquery.shuffle.min.js │ └── toc.js ├── licenses │ ├── LICENSE │ └── LICENSE-BSD-NAVGOCO.txt ├── markdown_merge.html ├── sidebar.json ├── sitemap.xml └── tooltips.json ├── index.ipynb ├── mail_settings.py ├── markdown_merge ├── __init__.py ├── _nbdev.py └── markdown_merge.py ├── settings.ini └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | ~* 3 | tmp* 4 | tags 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | env/ 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # dotenv 88 | .env 89 | 90 | # virtualenv 91 | .venv 92 | venv/ 93 | ENV/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | 108 | .vscode 109 | *.swp 110 | 111 | # osx generated files 112 | .DS_Store 113 | .DS_Store? 114 | .Trashes 115 | ehthumbs.db 116 | Thumbs.db 117 | .idea 118 | 119 | # pytest 120 | .pytest_cache 121 | -------------------------------------------------------------------------------- /00_markdown_merge.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# default_exp markdown_merge" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "# MarkdownMerge\n", 17 | "\n", 18 | "> API details" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 2, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "#export\n", 28 | "from django.core.mail import *\n", 29 | "from django.conf import *\n", 30 | "from markdown import markdown\n", 31 | "from email.headerregistry import Address\n", 32 | "from time import sleep\n", 33 | "import os" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": 5, 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "#hide\n", 43 | "from nbdev.showdoc import *" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": 7, 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "#export\n", 53 | "def get_addr(email, name=None):\n", 54 | " \"Convert `email` and optional `name` into an email `Address` object\"\n", 55 | " return Address(email if name is None else name, addr_spec=email)" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 8, 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "#export\n", 65 | "def md2email(subj, from_addr, to_addrs, md, conn=None, attach=None):\n", 66 | " \"Create a multipart (markdown HTML and text) email\"\n", 67 | " if not isinstance(to_addrs, (list,tuple)): to_addrs = [to_addrs]\n", 68 | " msg = EmailMultiAlternatives(subj, md, str(from_addr), [str(o) for o in to_addrs], connection=conn)\n", 69 | " msg.attach_alternative(markdown(md), \"text/html\")\n", 70 | " if attach is not None:\n", 71 | " if not isinstance(attach, (list,tuple)): attach = [attach]\n", 72 | " for att in attach: msg.attach_file(att)\n", 73 | " return msg" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 9, 79 | "metadata": {}, 80 | "outputs": [], 81 | "source": [ 82 | "#export\n", 83 | "class MarkdownMerge:\n", 84 | " \"Send a templated email merge message formatted with Markdown\"\n", 85 | " def __init__(self, addrs, from_addr, subj, msg, server_settings=None, inserts=None):\n", 86 | " if server_settings is None: os.environ['DJANGO_SETTINGS_MODULE'] = 'mail_settings'\n", 87 | " else:\n", 88 | " settings._wrapped=empty\n", 89 | " settings.configure(SECRET_KEY='XXX', **server_settings)\n", 90 | " self.addrs,self.from_addr,self.subj,self.msg,self.i = addrs,from_addr,subj,msg,0\n", 91 | " self.inserts = [{}]*len(addrs) if inserts is None else inserts\n", 92 | "\n", 93 | " def send_msgs(self, pause=0.5):\n", 94 | " \"Send all unsent messages to `addrs` with `pause` secs between each send\"\n", 95 | " with get_connection() as conn:\n", 96 | " while self.i < len(self.addrs):\n", 97 | " addr,insert = self.addrs[self.i],self.inserts[self.i]\n", 98 | " msg = self.msg.format(**insert)\n", 99 | " md2email(self.subj, self.from_addr, addr, md=msg, conn=conn).send()\n", 100 | " sleep(pause)\n", 101 | " self.i += 1\n", 102 | " if self.i%100==0: print(self.i)\n", 103 | "\n", 104 | " def set_test(self, test=True):\n", 105 | " \"When `True`, just print the messages to the console, don't send them\"\n", 106 | " backend = ('smtp','console')[test]\n", 107 | " settings.EMAIL_BACKEND = f'django.core.mail.backends.{backend}.EmailBackend'\n", 108 | "\n", 109 | " def reset(self):\n", 110 | " \"Reset sent message list, so `send_msgs` will start from first message\"\n", 111 | " self.i=0" 112 | ] 113 | }, 114 | { 115 | "cell_type": "markdown", 116 | "metadata": {}, 117 | "source": [ 118 | "Specify `from_addr` and `to_addrs` as either a string, or an `Address` object (created with `get_addr`). Note the `to_addrs` is a list." 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": 46, 124 | "metadata": {}, 125 | "outputs": [], 126 | "source": [ 127 | "from_addr = get_addr('from@example.com', 'Jeremy Howard')\n", 128 | "to_addrs = [get_addr('to@example.com', 'Jeremy')]" 129 | ] 130 | }, 131 | { 132 | "cell_type": "markdown", 133 | "metadata": {}, 134 | "source": [ 135 | "Your message should be in [markdown](https://daringfireball.net/projects/markdown/syntax) format. It will be converted into a two part email, containing both a plain text and an HTML part, so recipients will see whatever format they're set as their preference for viewing mail. Anything in curly brackets `{}` will be replaced with the contents of the inserts dictionary for that address. If there are no bracketed variables to replace, then you don't need to pass any inserts." 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": 10, 141 | "metadata": {}, 142 | "outputs": [], 143 | "source": [ 144 | "msg = \"\"\"\n", 145 | "**Hello there!**\n", 146 | "\n", 147 | "Here is your special message: *{special}*\n", 148 | "\"\"\"" 149 | ] 150 | }, 151 | { 152 | "cell_type": "markdown", 153 | "metadata": {}, 154 | "source": [ 155 | "`inserts` is a list of dictionaries. For each dictionary, the keys should match the bracketed names in your email template, and the values will be filled in to those sections." 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": 47, 161 | "metadata": {}, 162 | "outputs": [], 163 | "source": [ 164 | "inserts = [{'special': \"You are special.\"}]" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": 48, 170 | "metadata": {}, 171 | "outputs": [], 172 | "source": [ 173 | "ml = MarkdownMerge(to_addrs, from_addr, 'A message', msg=msg, inserts=inserts)" 174 | ] 175 | }, 176 | { 177 | "cell_type": "markdown", 178 | "metadata": {}, 179 | "source": [ 180 | "In the above example, we didn't specify any `server_settings`, so settings will be read from `mail_settings.py`. This is in Django settings format, so you can find complete docs for what can be provided in the [Django mail help](https://docs.djangoproject.com/en/2.2/topics/email/). A sample settings file is provided in the markdown_merge repo.\n", 181 | "\n", 182 | "Alternatively, pass a dictionary to `server_settings` with the required keys, for instance:" 183 | ] 184 | }, 185 | { 186 | "cell_type": "code", 187 | "execution_count": null, 188 | "metadata": {}, 189 | "outputs": [], 190 | "source": [ 191 | "cfg = dict(EMAIL_HOST='smtp.fastmail.com', EMAIL_PORT=465,\n", 192 | " EMAIL_HOST_USER='aa@example.com', EMAIL_HOST_PASSWORD='XXX', EMAIL_USE_SSL=True)" 193 | ] 194 | }, 195 | { 196 | "cell_type": "code", 197 | "execution_count": 14, 198 | "metadata": {}, 199 | "outputs": [ 200 | { 201 | "data": { 202 | "text/markdown": [ 203 | "

MarkdownMerge.set_test[source]

\n", 204 | "\n", 205 | "> MarkdownMerge.set_test(**`test`**=*`True`*)\n", 206 | "\n", 207 | "When `True`, just print the messages to the console, don't send them" 208 | ], 209 | "text/plain": [ 210 | "" 211 | ] 212 | }, 213 | "metadata": {}, 214 | "output_type": "display_data" 215 | } 216 | ], 217 | "source": [ 218 | "show_doc(MarkdownMerge.set_test)" 219 | ] 220 | }, 221 | { 222 | "cell_type": "code", 223 | "execution_count": 18, 224 | "metadata": {}, 225 | "outputs": [], 226 | "source": [ 227 | "ml.set_test(True)" 228 | ] 229 | }, 230 | { 231 | "cell_type": "code", 232 | "execution_count": 23, 233 | "metadata": {}, 234 | "outputs": [ 235 | { 236 | "data": { 237 | "text/markdown": [ 238 | "

MarkdownMerge.send_msgs[source]

\n", 239 | "\n", 240 | "> MarkdownMerge.send_msgs(**`pause`**=*`0.5`*)\n", 241 | "\n", 242 | "Send all unsent messages to `addrs` with `pause` secs between each send" 243 | ], 244 | "text/plain": [ 245 | "" 246 | ] 247 | }, 248 | "metadata": {}, 249 | "output_type": "display_data" 250 | } 251 | ], 252 | "source": [ 253 | "show_doc(MarkdownMerge.send_msgs)" 254 | ] 255 | }, 256 | { 257 | "cell_type": "code", 258 | "execution_count": 32, 259 | "metadata": {}, 260 | "outputs": [ 261 | { 262 | "name": "stdout", 263 | "output_type": "stream", 264 | "text": [ 265 | "Content-Type: multipart/alternative;\n", 266 | " boundary=\"===============7994339675932848898==\"\n", 267 | "MIME-Version: 1.0\n", 268 | "Subject: A message\n", 269 | "From: Jeremy Howard \n", 270 | "To: Jeremy \n", 271 | "Date: Mon, 25 Nov 2019 06:11:11 -0000\n", 272 | "Message-ID: <157466227172.47319.9791277819074450547@usf3>\n", 273 | "\n", 274 | "--===============7994339675932848898==\n", 275 | "Content-Type: text/plain; charset=\"utf-8\"\n", 276 | "MIME-Version: 1.0\n", 277 | "Content-Transfer-Encoding: 7bit\n", 278 | "\n", 279 | "\n", 280 | "**Hello there!**\n", 281 | "\n", 282 | "Here is your special message: *You are special.*\n", 283 | "\n", 284 | "--===============7994339675932848898==\n", 285 | "Content-Type: text/html; charset=\"utf-8\"\n", 286 | "MIME-Version: 1.0\n", 287 | "Content-Transfer-Encoding: 7bit\n", 288 | "\n", 289 | "

Hello there!

\n", 290 | "

Here is your special message: You are special.

\n", 291 | "--===============7994339675932848898==--\n", 292 | "\n", 293 | "-------------------------------------------------------------------------------\n" 294 | ] 295 | } 296 | ], 297 | "source": [ 298 | "ml.send_msgs(pause=0.5)" 299 | ] 300 | }, 301 | { 302 | "cell_type": "markdown", 303 | "metadata": {}, 304 | "source": [ 305 | "Use `pause` to avoid sending too many messages too quickly; many SMTP servers restrict sending speed to avoid abuse. If you get an error during sending (e.g. \"too many messages\"), then wait an hour or so, then continue sending, using a larger `pause` value.\n", 306 | "\n", 307 | "**NB**: You can just call `send_msgs` again when resending, since the successfully sent message count is saved, and those messages are not re-sent (unless you call `reset`). This includes test sends, therefore you should run reset after a test send." 308 | ] 309 | }, 310 | { 311 | "cell_type": "code", 312 | "execution_count": 29, 313 | "metadata": {}, 314 | "outputs": [ 315 | { 316 | "data": { 317 | "text/markdown": [ 318 | "

MarkdownMerge.reset[source]

\n", 319 | "\n", 320 | "> MarkdownMerge.reset()\n", 321 | "\n", 322 | "Reset send message list, so `send_msgs` will start from first message" 323 | ], 324 | "text/plain": [ 325 | "" 326 | ] 327 | }, 328 | "metadata": {}, 329 | "output_type": "display_data" 330 | } 331 | ], 332 | "source": [ 333 | "show_doc(MarkdownMerge.reset)" 334 | ] 335 | }, 336 | { 337 | "cell_type": "code", 338 | "execution_count": 31, 339 | "metadata": {}, 340 | "outputs": [], 341 | "source": [ 342 | "ml.reset()" 343 | ] 344 | }, 345 | { 346 | "cell_type": "markdown", 347 | "metadata": {}, 348 | "source": [ 349 | "## Utility functions" 350 | ] 351 | }, 352 | { 353 | "cell_type": "code", 354 | "execution_count": 27, 355 | "metadata": {}, 356 | "outputs": [ 357 | { 358 | "data": { 359 | "text/markdown": [ 360 | "

md2email[source]

\n", 361 | "\n", 362 | "> md2email(**`subj`**, **`from_addr`**, **`to_addrs`**, **`md`**, **`conn`**=*`None`*, **`attach`**=*`None`*)\n", 363 | "\n", 364 | "Create a multipart (markdown HTML and text) email" 365 | ], 366 | "text/plain": [ 367 | "" 368 | ] 369 | }, 370 | "metadata": {}, 371 | "output_type": "display_data" 372 | } 373 | ], 374 | "source": [ 375 | "show_doc(md2email)" 376 | ] 377 | }, 378 | { 379 | "cell_type": "code", 380 | "execution_count": 36, 381 | "metadata": {}, 382 | "outputs": [], 383 | "source": [ 384 | "eml = md2email(\"hi\", from_addr, to_addrs, msg)" 385 | ] 386 | }, 387 | { 388 | "cell_type": "markdown", 389 | "metadata": {}, 390 | "source": [ 391 | "The basic email body is the plain text message (note that the template variables in `{}` will be filled in by `MarkdownMerge`):" 392 | ] 393 | }, 394 | { 395 | "cell_type": "code", 396 | "execution_count": 45, 397 | "metadata": {}, 398 | "outputs": [ 399 | { 400 | "name": "stdout", 401 | "output_type": "stream", 402 | "text": [ 403 | "\n", 404 | "**Hello there!**\n", 405 | "\n", 406 | "Here is your special message: *{special}*\n", 407 | "\n" 408 | ] 409 | } 410 | ], 411 | "source": [ 412 | "print(eml.body)" 413 | ] 414 | }, 415 | { 416 | "cell_type": "markdown", 417 | "metadata": {}, 418 | "source": [ 419 | "Most email software is set up to display the HTML version:" 420 | ] 421 | }, 422 | { 423 | "cell_type": "code", 424 | "execution_count": 44, 425 | "metadata": {}, 426 | "outputs": [ 427 | { 428 | "data": { 429 | "text/html": [ 430 | "

Hello there!

\n", 431 | "

Here is your special message: {special}

" 432 | ], 433 | "text/plain": [ 434 | "" 435 | ] 436 | }, 437 | "execution_count": 44, 438 | "metadata": {}, 439 | "output_type": "execute_result" 440 | } 441 | ], 442 | "source": [ 443 | "from IPython.display import HTML\n", 444 | "html,mimetype = eml.alternatives[0]\n", 445 | "HTML(html)" 446 | ] 447 | }, 448 | { 449 | "cell_type": "code", 450 | "execution_count": 28, 451 | "metadata": {}, 452 | "outputs": [ 453 | { 454 | "data": { 455 | "text/markdown": [ 456 | "

get_addr[source]

\n", 457 | "\n", 458 | "> get_addr(**`email`**, **`name`**=*`None`*)\n", 459 | "\n", 460 | "Convert `email` and optional `name` into an email `Address` object" 461 | ], 462 | "text/plain": [ 463 | "" 464 | ] 465 | }, 466 | "metadata": {}, 467 | "output_type": "display_data" 468 | } 469 | ], 470 | "source": [ 471 | "show_doc(get_addr)" 472 | ] 473 | }, 474 | { 475 | "cell_type": "code", 476 | "execution_count": 6, 477 | "metadata": {}, 478 | "outputs": [ 479 | { 480 | "name": "stdout", 481 | "output_type": "stream", 482 | "text": [ 483 | "Converted 00_markdownmail.ipynb.\n", 484 | "Converted 99_index.ipynb.\n" 485 | ] 486 | } 487 | ], 488 | "source": [ 489 | "#hide\n", 490 | "from nbdev.export import notebook2script\n", 491 | "notebook2script()" 492 | ] 493 | }, 494 | { 495 | "cell_type": "code", 496 | "execution_count": null, 497 | "metadata": {}, 498 | "outputs": [], 499 | "source": [] 500 | } 501 | ], 502 | "metadata": { 503 | "kernelspec": { 504 | "display_name": "Python 3", 505 | "language": "python", 506 | "name": "python3" 507 | }, 508 | "language_info": { 509 | "codemirror_mode": { 510 | "name": "ipython", 511 | "version": 3 512 | }, 513 | "file_extension": ".py", 514 | "mimetype": "text/x-python", 515 | "name": "python", 516 | "nbconvert_exporter": "python", 517 | "pygments_lexer": "ipython3", 518 | "version": "3.7.4" 519 | } 520 | }, 521 | "nbformat": 4, 522 | "nbformat_minor": 2 523 | } 524 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRC = $(wildcard ./*.ipynb) 2 | 3 | all: markdown_merge docs 4 | 5 | markdown_merge: $(SRC) 6 | nbdev_build_lib 7 | touch markdown_merge 8 | 9 | docs_serve: docs 10 | cd docs && bundle exec jekyll serve 11 | 12 | docs: $(SRC) 13 | nbdev_build_docs 14 | touch docs 15 | 16 | pypi: dist 17 | twine upload --repository pypi dist/* 18 | 19 | dist: clean 20 | python setup.py sdist bdist_wheel 21 | 22 | clean: 23 | rm -rf dist 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | # MarkdownMerge 12 | 13 | Send templated emails in markdown. 14 | 15 | 16 | ## Install 17 | 18 | `pip install markdown_merge` 19 | 20 | ## How to use 21 | 22 | ### Provide your SMTP server settings, e.g. for fastmail 23 |
24 |
25 | 26 | ```python 27 | cfg = dict(EMAIL_HOST='smtp.fastmail.com', EMAIL_PORT=465, 28 | EMAIL_HOST_USER='XXX@fastmail.com', EMAIL_HOST_PASSWORD='XXX', EMAIL_USE_SSL=True) 29 | ``` 30 | 31 |
32 | 33 |
34 | 35 | Alternately you can put your server settings in `mail_settings.py`. There's an example settings file in the repo. 36 | 37 | ### Provide your email details 38 |
39 |
40 | 41 | ```python 42 | from_addr = get_addr('XXX@fastmail.com', 'Jeremy Howard') 43 | to_addrs = [get_addr('douglas@example.com', 'Douglas Adams'), 44 | get_addr('cleese@example.com', 'John Cleese')] 45 | inserts = [{'special': "Thanks for all the fish."}, 46 | {'special': "That was a silly walk."}] 47 | 48 | msg = """## Hello there! 49 | 50 | Here is your special message: *{special}*""" 51 | ``` 52 | 53 |
54 | 55 |
56 |
57 |
58 | 59 | ```python 60 | ml = MarkdownMerge(to_addrs, from_addr, 'A message', msg=msg, inserts=inserts) 61 | ``` 62 | 63 |
64 | 65 |
66 | 67 | Optionally, enable *test* mode to just print the messages, instead of sending them. 68 |
69 |
70 | 71 | ```python 72 | ml.set_test(True) 73 | ``` 74 | 75 |
76 | 77 |
78 | 79 | ### Send your messages 80 |
81 |
82 | 83 | ```python 84 | ml.send_msgs() 85 | ``` 86 | 87 |
88 | 89 |
90 | 91 | ## Credits 92 | 93 | All the hard work is done by Django mail, python-markdown, and python. So thanks to the authors of those projects! 94 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _site/ 2 | -------------------------------------------------------------------------------- /docs/00*: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/markdown_merge/87570b90246fe74b4537bd448b672c84604f4228/docs/00* -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'github-pages', group: :jekyll_plugins 4 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | baseurl: /markdown_merge 2 | repository: jph00/markdown_merge 3 | output: web 4 | topnav_title: MarkdownMailMerge 5 | site_title: MarkdownMailMerge 6 | company_name: fast.ai 7 | google_analytics: 8 | google_search: 9 | description: "Send templated emails in markdown" 10 | 11 | host: 127.0.0.1 12 | # the preview server used. Leave as is. 13 | port: 4000 14 | # the port where the preview is rendered. 15 | 16 | exclude: 17 | - .idea/ 18 | - .gitignore 19 | - vendor 20 | 21 | exclude: [vendor] 22 | 23 | highlighter: rouge 24 | markdown: kramdown 25 | kramdown: 26 | input: GFM 27 | auto_ids: true 28 | hard_wrap: false 29 | syntax_highlighter: rouge 30 | 31 | collections: 32 | tooltips: 33 | output: false 34 | 35 | defaults: 36 | - 37 | scope: 38 | path: "" 39 | type: "pages" 40 | values: 41 | layout: "page" 42 | comments: true 43 | search: true 44 | sidebar: home_sidebar 45 | topnav: topnav 46 | - 47 | scope: 48 | path: "" 49 | type: "tooltips" 50 | values: 51 | layout: "page" 52 | comments: true 53 | search: true 54 | tooltip: true 55 | 56 | sidebars: 57 | - home_sidebar 58 | 59 | theme: jekyll-theme-cayman 60 | 61 | -------------------------------------------------------------------------------- /docs/_data/alerts.yml: -------------------------------------------------------------------------------- 1 | tip: '