-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'origin/develop'
- Loading branch information
Showing
20 changed files
with
1,007 additions
and
1,115 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# GitHub Continuous Integration Configuration | ||
name: CI | ||
|
||
# Define conditions for when to run this action | ||
on: | ||
pull_request: # Run on all pull requests | ||
push: # Run on all pushes to master | ||
branches: | ||
- main | ||
- develop | ||
|
||
# Allows you to run this workflow manually from the Actions tab | ||
workflow_dispatch: | ||
|
||
# A workflow run is made up of one or more jobs. Each job has an id, for | ||
# example, one of our jobs is "lint" | ||
jobs: | ||
test: | ||
name: Tests ${{ matrix.python-version }} | ||
runs-on: ${{ matrix.os }} | ||
strategy: | ||
# Define OS and Python versions to use. 3.x is the latest minor version. | ||
matrix: | ||
python-version: ["3.6", "3.7", "3.8", "3.9", "3.x"] | ||
os: [ubuntu-latest] | ||
|
||
# Sequence of tasks for this job | ||
steps: | ||
# Check out latest code | ||
# Docs: https://github.com/actions/checkout | ||
- name: Checkout code | ||
uses: actions/checkout@v2 | ||
|
||
# Set up Python | ||
# Docs: https://github.com/actions/setup-python | ||
- name: Set up Python ${{ matrix.python-version }} | ||
uses: actions/setup-python@v2 | ||
with: | ||
python-version: ${{ matrix.python-version }} | ||
|
||
# Install dependencies | ||
# https://github.com/ymyzk/tox-gh-actions#workflow-configuration | ||
- name: Install dependencies | ||
run: | | ||
python -m pip install --upgrade pip | ||
pip install coverage tox tox-gh-actions | ||
# Run tests | ||
# https://github.com/ymyzk/tox-gh-actions#workflow-configuration | ||
- name: Run tests | ||
run: tox | ||
- name: Combine coverage | ||
run: coverage xml | ||
|
||
# Upload coverage report | ||
# https://github.com/codecov/codecov-action | ||
- name: Upload coverage report | ||
uses: codecov/codecov-action@v1 | ||
with: | ||
fail_ci_if_error: true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,3 +27,4 @@ build/ | |
/tmp/ | ||
/.coverage* | ||
*,cover | ||
coverage.xml |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,33 +3,15 @@ | |
Andrew DeOrio <[email protected]> | ||
""" | ||
from __future__ import print_function | ||
import sys | ||
import time | ||
import codecs | ||
import textwrap | ||
from pathlib import Path | ||
import csv | ||
import click | ||
from .template_message import TemplateMessage | ||
from .sendmail_client import SendmailClient | ||
from . import exceptions | ||
from . import utils | ||
|
||
# Python 2 pathlib support requires backport | ||
try: | ||
from pathlib2 import Path | ||
except ImportError: | ||
from pathlib import Path | ||
|
||
# Python 2 UTF8 support requires csv backport | ||
try: | ||
from backports import csv | ||
except ImportError: | ||
import csv | ||
|
||
# Python 2 UTF8 file redirection | ||
# http://www.macfreek.nl/memory/Encoding_of_Python_stdout | ||
if sys.stdout.encoding != 'UTF-8' and not hasattr(sys.stdout, "buffer"): | ||
sys.stdout = codecs.getwriter('utf-8')(sys.stdout, 'strict') | ||
|
||
|
||
@click.command(context_settings={"help_option_names": ['-h', '--help']}) | ||
|
@@ -128,37 +110,28 @@ def main(sample, dry_run, limit, no_limit, resume, | |
break | ||
time.sleep(1) | ||
print_bright_white_on_cyan( | ||
">>> message {message_num}" | ||
.format(message_num=message_num), | ||
f">>> message {message_num}", | ||
output_format, | ||
) | ||
print_message(message, output_format) | ||
print_bright_white_on_cyan( | ||
">>> message {message_num} sent" | ||
.format(message_num=message_num), | ||
f">>> message {message_num} sent", | ||
output_format, | ||
) | ||
message_num += 1 | ||
|
||
except exceptions.MailmergeError as error: | ||
hint_text = '\nHint: "--resume {}"'.format(message_num) | ||
sys.exit( | ||
"Error on message {message_num}\n" | ||
"{error}" | ||
"{hint}" | ||
.format( | ||
message_num=message_num, | ||
error=error, | ||
hint=(hint_text if message_num > 1 else ""), | ||
) | ||
) | ||
hint_text = "" | ||
if message_num > 1: | ||
hint_text = f'\nHint: "--resume {message_num}"' | ||
sys.exit(f"Error on message {message_num}\n{error}{hint_text}") | ||
|
||
# Hints for user | ||
if not no_limit: | ||
pluralizer = "" if limit == 1 else "s" | ||
print( | ||
">>> Limit was {limit} message{pluralizer}. " | ||
f">>> Limit was {limit} message{pluralizer}. " | ||
"To remove the limit, use the --no-limit option." | ||
.format(limit=limit, pluralizer=("" if limit == 1 else "s")) | ||
) | ||
if dry_run: | ||
print( | ||
|
@@ -180,40 +153,40 @@ def check_input_files(template_path, database_path, config_path, sample): | |
sys.exit(0) | ||
|
||
if not template_path.exists(): | ||
sys.exit(textwrap.dedent(u"""\ | ||
sys.exit(textwrap.dedent(f"""\ | ||
Error: can't find template "{template_path}". | ||
Create a sample (--sample) or specify a file (--template). | ||
See https://github.com/awdeorio/mailmerge for examples.\ | ||
""".format(template_path=template_path))) | ||
""")) | ||
|
||
if not database_path.exists(): | ||
sys.exit(textwrap.dedent(u"""\ | ||
sys.exit(textwrap.dedent(f"""\ | ||
Error: can't find database "{database_path}". | ||
Create a sample (--sample) or specify a file (--database). | ||
See https://github.com/awdeorio/mailmerge for examples.\ | ||
""".format(database_path=database_path))) | ||
""")) | ||
|
||
if not config_path.exists(): | ||
sys.exit(textwrap.dedent(u"""\ | ||
sys.exit(textwrap.dedent(f"""\ | ||
Error: can't find config "{config_path}". | ||
Create a sample (--sample) or specify a file (--config). | ||
See https://github.com/awdeorio/mailmerge for examples.\ | ||
""".format(config_path=config_path))) | ||
""")) | ||
|
||
|
||
def create_sample_input_files(template_path, database_path, config_path): | ||
"""Create sample template, database and server config.""" | ||
for path in [template_path, database_path, config_path]: | ||
if path.exists(): | ||
sys.exit("Error: file exists: {}".format(path)) | ||
sys.exit(f"Error: file exists: {path}") | ||
with template_path.open("w") as template_file: | ||
template_file.write(textwrap.dedent(u"""\ | ||
template_file.write(textwrap.dedent("""\ | ||
TO: {{email}} | ||
SUBJECT: Testing mailmerge | ||
FROM: My Self <[email protected]> | ||
|
@@ -223,13 +196,13 @@ def create_sample_input_files(template_path, database_path, config_path): | |
Your number is {{number}}. | ||
""")) | ||
with database_path.open("w") as database_file: | ||
database_file.write(textwrap.dedent(u"""\ | ||
database_file.write(textwrap.dedent("""\ | ||
email,name,number | ||
[email protected],"Myself",17 | ||
[email protected],"Bob",42 | ||
""")) | ||
with config_path.open("w") as config_file: | ||
config_file.write(textwrap.dedent(u"""\ | ||
config_file.write(textwrap.dedent("""\ | ||
# Mailmerge SMTP Server Config | ||
# https://github.com/awdeorio/mailmerge | ||
# | ||
|
@@ -273,17 +246,13 @@ def create_sample_input_files(template_path, database_path, config_path): | |
# port = 25 | ||
# ratelimit = 0 | ||
""")) | ||
print(textwrap.dedent(u"""\ | ||
print(textwrap.dedent(f"""\ | ||
Created sample template email "{template_path}" | ||
Created sample database "{database_path}" | ||
Created sample config file "{config_path}" | ||
Edit these files, then run mailmerge again.\ | ||
""".format( | ||
template_path=template_path, | ||
database_path=database_path, | ||
config_path=config_path, | ||
))) | ||
""")) | ||
|
||
|
||
def read_csv_database(database_path): | ||
|
@@ -292,20 +261,25 @@ def read_csv_database(database_path): | |
We'll use a class to modify the csv library's default dialect ('excel') to | ||
enable strict syntax checking. This will trigger errors for things like | ||
unclosed quotes. | ||
We open the file with the utf-8-sig encoding, which skips a byte order mark | ||
(BOM), if any. Sometimes Excel will save CSV files with a BOM. See Issue | ||
#93 https://github.com/awdeorio/mailmerge/issues/93 | ||
""" | ||
class StrictExcel(csv.excel): | ||
# Our helper class is really simple | ||
# pylint: disable=too-few-public-methods, missing-class-docstring | ||
strict = True | ||
|
||
with database_path.open(mode="r", encoding="utf-8") as database_file: | ||
with database_path.open(encoding="utf-8-sig") as database_file: | ||
reader = csv.DictReader(database_file, dialect=StrictExcel) | ||
try: | ||
for row in reader: | ||
yield row | ||
except csv.Error as err: | ||
raise exceptions.MailmergeError( | ||
"{}:{}: {}".format(database_path, reader.line_num, err) | ||
f"{database_path}:{reader.line_num}: {err}" | ||
) | ||
|
||
|
||
|
@@ -343,11 +317,11 @@ def print_message(message, output_format): | |
assert output_format in ["colorized", "text", "raw"] | ||
|
||
if output_format == "raw": | ||
print(utils.flatten_message(message)) | ||
print(message) | ||
return | ||
|
||
for header, value in message.items(): | ||
print(u"{header}: {value}".format(header=header, value=value)) | ||
print(f"{header}: {value}") | ||
print() | ||
for part in message.walk(): | ||
if part.get_content_maintype() == "multipart": | ||
|
@@ -356,23 +330,20 @@ def print_message(message, output_format): | |
if message.is_multipart(): | ||
# Only print message part dividers for multipart messages | ||
print_cyan( | ||
">>> message part: {content_type}" | ||
.format(content_type=part.get_content_type()), | ||
f">>> message part: {part.get_content_type()}", | ||
output_format, | ||
) | ||
charset = str(part.get_charset()) | ||
print(part.get_payload(decode=True).decode(charset)) | ||
print() | ||
elif is_attachment(part): | ||
print_cyan( | ||
">>> message part: attachment {filename}" | ||
.format(filename=part.get_filename()), | ||
f">>> message part: attachment {part.get_filename()}", | ||
output_format, | ||
) | ||
else: | ||
print_cyan( | ||
">>> message part: {content_type}" | ||
.format(content_type=part.get_content_type()), | ||
f">>> message part: {part.get_content_type()}", | ||
output_format, | ||
) | ||
|
||
|
Oops, something went wrong.