Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Continue on errror #118

Closed
wants to merge 9 commits into from
102 changes: 73 additions & 29 deletions mailmerge/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
import click
from .template_message import TemplateMessage
from .sendmail_client import SendmailClient
from . import exceptions
from .exceptions import MailmergeError
from .log import MailmergeLog
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would lean towards using Python's standard library logging mechanism over building a custom logger.

from . import utils

# Python 2 pathlib support requires backport
Expand Down Expand Up @@ -55,6 +56,10 @@
type=click.IntRange(1, None),
help="Start on message number INTEGER",
)
@click.option(
"--continue-on-error/--no-continue-on-error", default=False,
help="(Do not) continue program if an error is encountered"
)
@click.option(
"--template", "template_path",
default="mailmerge_template.txt",
Expand All @@ -79,9 +84,15 @@
type=click.Choice(["colorized", "text", "raw"]),
help="Output format (colorized).",
)
@click.option(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that creating a log by default is reasonable. Maybe we can just call it mailmerge.log? That would simplify the command line interface.

"--log", "log_path",
default="",
type=click.Path(),
help="log CSV (not created by default)",
)
def main(sample, dry_run, limit, no_limit, resume,
template_path, database_path, config_path,
output_format):
output_format, continue_on_error, log_path):
"""
Mailmerge is a simple, command line mail merge tool.

Expand All @@ -108,37 +119,42 @@ def main(sample, dry_run, limit, no_limit, resume,

# Run
message_num = 1 + start
recipients = []
try:
log = MailmergeLog(log_path)
template_message = TemplateMessage(template_path)
csv_database = read_csv_database(database_path)
sendmail_client = SendmailClient(config_path, dry_run)
for _, row in enumerate_range(csv_database, start, stop):
sender, recipients, message = template_message.render(row)
sendmail_client.sendmail(sender, recipients, message)
print_bright_white_on_cyan(
">>> message {message_num}"
.format(message_num=message_num),
output_format,
)
print_message(message, output_format)
print_bright_white_on_cyan(
">>> message {message_num} sent"
.format(message_num=message_num),
output_format,
)
try:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this logic can be simplified after merging in the latest upstream develop. That includes a try-except block in the message send part.

recipients = []
sender, recipients, message = template_message.render(row)
sendmail_client.sendmail(sender, recipients, message)
print_bright_white_on_cyan(
">>> message {message_num}"
.format(message_num=message_num),
output_format,
)
print_message(message, output_format)
print_bright_white_on_cyan(
">>> message {message_num} sent"
.format(message_num=message_num),
output_format,
)
except MailmergeError as error:
log.log(message_num, recipients, error)
print_error(
error, message_num, output_format, continue_on_error
)
else:
if dry_run:
log.log(message_num, recipients, "OK, not sent")
else:
log.log(message_num, recipients, "Sent")
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 ""),
)
)
except MailmergeError as error:
log.log(message_num, recipients, error)
print_error(error, message_num, output_format, False)

# Hints for user
if not no_limit:
Expand All @@ -153,6 +169,9 @@ def main(sample, dry_run, limit, no_limit, resume,
"To send messages, use the --no-dry-run option."
)

# Close log
log.close()


if __name__ == "__main__":
# No value for parameter, that's how click works
Expand Down Expand Up @@ -277,9 +296,9 @@ class StrictExcel(csv.excel):
for row in reader:
yield row
except csv.Error as err:
raise exceptions.MailmergeError(
raise MailmergeError(
"{}:{}: {}".format(database_path, reader.line_num, err)
)
) from err


def enumerate_range(iterable, start=0, stop=None):
Expand Down Expand Up @@ -311,6 +330,31 @@ def print_bright_white_on_cyan(string, output_format):
print(string)


def print_error(error, message_num, output_format, continue_on_error):
"""Print string to stdout, optionally enabling color."""
if message_num > 1 and not continue_on_error:
hint_text = '\nHint: "--resume {}"'.format(message_num)
else:
hint_text = ""
error_message = (
"Error on message {message_num}\n"
"{error}"
"{hint}"
.format(
message_num=message_num,
error=error,
hint=hint_text,
)
)

if output_format == "colorized":
error_message = "\x1b[31m" + error_message + "\x1b(B\x1b[m"
if continue_on_error:
print(error_message, file=sys.stderr)
else:
sys.exit(error_message)


def print_message(message, output_format):
"""Print a message with colorized output."""
assert output_format in ["colorized", "text", "raw"]
Expand Down
65 changes: 65 additions & 0 deletions mailmerge/log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""
SMTP client reads configuration and sends message.

Andrew DeOrio <[email protected]>
"""
import os.path

from .exceptions import MailmergeError


class MailmergeLog():
"""Represents a log file."""

def __init__(self, path, dry_run=False):
"""Open a log file for output."""
self.path = path
self.dry_run = dry_run
try:
if self.path:
if os.path.isfile(self.path):
self.file = open(self.path, "a+")
else:
self.file = open(self.path, "w+")
print("number,email,log", file=self.file)
else:
self.file = None
except MailmergeError as err:
raise MailmergeError(
"{}: {}".format(
self.path, "Unable to open log file"
)
) from err

def log(self, number, recipients, message):
"""Write a message to a log file."""
if self.file:
if len(recipients) == 0:
recipients = [""]
try:
for email in recipients:
print(
"{},{},\"{}\"".format(
number, email, message
),
file=self.file
)
except MailmergeError as err:
raise MailmergeError(
"{}: {}".format(
self.path, "Unable to write to logfile"
)
) from err

def close(self):
"""Close a log file."""
if self.file:
try:
self.file.close()
self.file = None
except MailmergeError as err:
raise MailmergeError(
"{}: {}".format(
self.path, "Unable to close logfile"
)
) from err
116 changes: 116 additions & 0 deletions tests/test_main_log_and_continue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# coding=utf-8
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More ideas for tests:

  • Log file already exists

# Python 2 source containing unicode https://www.python.org/dev/peps/pep-0263/
"""
System tests.

Andrew DeOrio <[email protected]>

pytest tmpdir docs:
http://doc.pytest.org/en/latest/tmpdir.html#the-tmpdir-fixture
"""
import textwrap
import sh

# Python 2 pathlib support requires backport
try:
from pathlib2 import Path
except ImportError:
from pathlib import Path

# The sh library triggers lot of false no-member errors
# pylint: disable=no-member


def test_log(tmpdir):
"""Verify --continue-on-error continues if theere is an error."""

# Second attachment
attachment2_path = Path(tmpdir/"attachment2.txt")
attachment2_path.write_text(u"Hello mailmerge\n")

# Simple template
template_path = Path(tmpdir/"mailmerge_template.txt")
template_path.write_text(textwrap.dedent(u"""\
TO: {{email}}
FROM: [email protected]
ATTACHMENT: {{attachment}}

Hello world
"""))

# Simple database with two entries
database_path = Path(tmpdir/"mailmerge_database.csv")
database_path.write_text(textwrap.dedent(u"""\
email,attachment
[email protected],attachment1.txt
[email protected],attachment2.txt
"""))

# Simple unsecure server config
config_path = Path(tmpdir/"mailmerge_server.conf")
config_path.write_text(textwrap.dedent(u"""\
[smtp_server]
host = open-smtp.example.com
port = 25
"""))

# Run mailmerge
log = tmpdir.join('log.csv')
with tmpdir.as_cwd():
output = sh.mailmerge(
"--no-limit", "--continue-on-error",
"--log", "log.csv"
)
stderr = output.stderr.decode("utf-8")
assert "message 1 sent" not in output
assert "message 2 sent" in output
assert "Error on message 1" in stderr
assert "Attachment not found" in stderr
assert log.read() == textwrap.dedent(u"""\
number,email,log
1,,"Attachment not found: {}/attachment1.txt"
2,[email protected],"OK, not sent"
""".format(tmpdir))


def test_continue_on_error(tmpdir):
"""Verify --continue-on-error continues if theere is an error."""

# Second attachment
attachment2_path = Path(tmpdir/"attachment2.txt")
attachment2_path.write_text(u"Hello mailmerge\n")

# Simple template
template_path = Path(tmpdir/"mailmerge_template.txt")
template_path.write_text(textwrap.dedent(u"""\
TO: {{email}}
FROM: [email protected]
ATTACHMENT: {{attachment}}

Hello world
"""))

# Simple database with two entries
database_path = Path(tmpdir/"mailmerge_database.csv")
database_path.write_text(textwrap.dedent(u"""\
email,attachment
[email protected],attachment1.txt
[email protected],attachment2.txt
"""))

# Simple unsecure server config
config_path = Path(tmpdir/"mailmerge_server.conf")
config_path.write_text(textwrap.dedent(u"""\
[smtp_server]
host = open-smtp.example.com
port = 25
"""))

# Run mailmerge
with tmpdir.as_cwd():
output = sh.mailmerge("--no-limit", "--continue-on-error")
stderr = output.stderr.decode("utf-8")
assert "message 1 sent" not in output
assert "message 2 sent" in output
assert "Error on message 1" in stderr
assert "Attachment not found" in stderr