Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
awdeorio committed Oct 8, 2021
2 parents 0ca52cf + 7861bb3 commit 5e479de
Show file tree
Hide file tree
Showing 20 changed files with 1,007 additions and 1,115 deletions.
60 changes: 60 additions & 0 deletions .github/workflows/main.yml
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ build/
/tmp/
/.coverage*
*,cover
coverage.xml
31 changes: 0 additions & 31 deletions .travis.yml

This file was deleted.

11 changes: 1 addition & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,6 @@ $ which mailmerge
/Users/awdeorio/src/mailmerge/env/bin/mailmerge
```

### Python2 development environment
Mailmerge is tested to work in both Python 2 and Python 3. Set up a Python 2 virtual environment.
```console
$ virtualenv -p python2 env2
$ source env2/bin/activate
$ pip install -e .[dev,test]
```

## Testing and code quality
Run unit tests
```console
Expand All @@ -42,8 +34,7 @@ $ pylint mailmerge tests setup.py
$ check-manifest
```

Test Python 2 and Python 3 compatibility. This will automatically create virtual environments and run all style and functional tests in each environment. Use `pyenv` to provide different versions of Python.
Run linters and tests in a clean environment. This will automatically create a temporary virtual environment.
```console
$ eval "$(pyenv init -)"
$ tox
```
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
Mailmerge
=========

[![PyPI](https://img.shields.io/pypi/v/mailmerge.svg)](https://pypi.org/project/mailmerge/)
[![Build Status](https://travis-ci.com/awdeorio/mailmerge.svg?branch=develop)](https://travis-ci.com/awdeorio/mailmerge)
[![CI main](https://github.com/awdeorio/mailmerge/workflows/CI/badge.svg?branch=develop)](https://github.com/awdeorio/mailmerge/actions?query=branch%3Adevelop)
[![codecov](https://codecov.io/gh/awdeorio/mailmerge/branch/develop/graph/badge.svg)](https://codecov.io/gh/awdeorio/mailmerge)
[![PyPI](https://img.shields.io/pypi/v/mailmerge.svg)](https://pypi.org/project/mailmerge/)

A simple, command line mail merge tool.

Expand Down
97 changes: 34 additions & 63 deletions mailmerge/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,15 @@
Andrew DeOrio <awdeorio@umich.edu>
"""
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']})
Expand Down Expand Up @@ -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(
Expand All @@ -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 <myself@mydomain.com>
Expand All @@ -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
myself@mydomain.com,"Myself",17
bob@bobdomain.com,"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
#
Expand Down Expand Up @@ -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):
Expand All @@ -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}"
)


Expand Down Expand Up @@ -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":
Expand All @@ -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,
)

Expand Down
Loading

0 comments on commit 5e479de

Please sign in to comment.