Repository: slarse/pdfebc

Issue:
Move all configurations to a common config file
Right now, the `email.cnf` file only includes `user`, `password` and `receiver` values. It shold be renamed `config.cnf` and include EVERYTHING that the user may want to customize.

- [x] Everything currently in `email.cnf`
- [x]  SMTP server and port
- [x] All defaults that are currently set in the CLI (e.g. `Ghostscript` binary default).

This is a breaking change and should be added last, before version 0.2.0 is released.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/slarse/pdfebc/pull/22?src=pr&el=h1) Report
> Merging [#22](https://codecov.io/gh/slarse/pdfebc/pull/22?src=pr&el=desc) into [master](https://codecov.io/gh/slarse/pdfebc/commit/95bdd8a5cc56f82d7f77678a88d50d7be9762995?src=pr&el=desc) will **decrease** coverage by `1.26%`.
> The diff coverage is `77.14%`.

[![Impacted file tree graph](https://codecov.io/gh/slarse/pdfebc/pull/22/graphs/tree.svg?token=FOgEpW164E&src=pr&width=650&height=150)](https://codecov.io/gh/slarse/pdfebc/pull/22?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #22      +/-   ##
==========================================
- Coverage    70.5%   69.23%   -1.27%     
==========================================
  Files           4        4              
  Lines         200      247      +47     
==========================================
+ Hits          141      171      +30     
- Misses         59       76      +17
```


| [Impacted Files](https://codecov.io/gh/slarse/pdfebc/pull/22?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pdfebc/main.py](https://codecov.io/gh/slarse/pdfebc/pull/22?src=pr&el=tree#diff-cGRmZWJjL21haW4ucHk=) | `0% <0%> (ø)` | :arrow_up: |
| [pdfebc/cli.py](https://codecov.io/gh/slarse/pdfebc/pull/22?src=pr&el=tree#diff-cGRmZWJjL2NsaS5weQ==) | `56.86% <20%> (-4.01%)` | :arrow_down: |
| [pdfebc/utils.py](https://codecov.io/gh/slarse/pdfebc/pull/22?src=pr&el=tree#diff-cGRmZWJjL3V0aWxzLnB5) | `94.87% <94.64%> (-3.93%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/slarse/pdfebc/pull/22?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/slarse/pdfebc/pull/22?src=pr&el=footer). Last update [95bdd8a...79197d3](https://codecov.io/gh/slarse/pdfebc/pull/22?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

codecov[bot]: # [Codecov](https://codecov.io/gh/slarse/pdfebc/pull/22?src=pr&el=h1) Report
> Merging [#22](https://codecov.io/gh/slarse/pdfebc/pull/22?src=pr&el=desc) into [master](https://codecov.io/gh/slarse/pdfebc/commit/95bdd8a5cc56f82d7f77678a88d50d7be9762995?src=pr&el=desc) will **decrease** coverage by `1.26%`.
> The diff coverage is `77.14%`.

[![Impacted file tree graph](https://codecov.io/gh/slarse/pdfebc/pull/22/graphs/tree.svg?width=650&height=150&token=FOgEpW164E&src=pr)](https://codecov.io/gh/slarse/pdfebc/pull/22?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #22      +/-   ##
==========================================
- Coverage    70.5%   69.23%   -1.27%     
==========================================
  Files           4        4              
  Lines         200      247      +47     
==========================================
+ Hits          141      171      +30     
- Misses         59       76      +17
```


| [Impacted Files](https://codecov.io/gh/slarse/pdfebc/pull/22?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pdfebc/main.py](https://codecov.io/gh/slarse/pdfebc/pull/22?src=pr&el=tree#diff-cGRmZWJjL21haW4ucHk=) | `0% <0%> (ø)` | :arrow_up: |
| [pdfebc/cli.py](https://codecov.io/gh/slarse/pdfebc/pull/22?src=pr&el=tree#diff-cGRmZWJjL2NsaS5weQ==) | `56.86% <20%> (-4.01%)` | :arrow_down: |
| [pdfebc/utils.py](https://codecov.io/gh/slarse/pdfebc/pull/22?src=pr&el=tree#diff-cGRmZWJjL3V0aWxzLnB5) | `94.87% <94.64%> (-3.93%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/slarse/pdfebc/pull/22?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/slarse/pdfebc/pull/22?src=pr&el=footer). Last update [95bdd8a...79197d3](https://codecov.io/gh/slarse/pdfebc/pull/22?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/pdfebc/cli.py b/pdfebc/cli.py
index 9b38bd1..40bb5b3 100644
--- a/pdfebc/cli.py
+++ b/pdfebc/cli.py
@@ -9,6 +9,7 @@
 """
 import argparse
 import sys
+from . import utils
 
 OUTPUT_DIR_DEFAULT = "pdfebc_out"
 SOURCE_DIR_DEFAULT = "."
@@ -41,16 +42,20 @@ def create_argparser():
     Returns:
         argparse.ArgumentParser: The argument parser for pdfebc.
     """
+    config = utils.read_config()
+    out_dir_default = utils.try_get_conf(config, utils.DEFAULT_SECTION_KEY, utils.OUT_DEFAULT_DIR_KEY)
+    src_dir_default = utils.try_get_conf(config, utils.DEFAULT_SECTION_KEY, utils.SRC_DEFAULT_DIR_KEY)
+    gs_default_binary = utils.try_get_conf(config, utils.DEFAULT_SECTION_KEY, utils.GS_DEFAULT_BINARY_KEY)
     parser = argparse.ArgumentParser(
         description=DESCRIPTION
         )
     parser.add_argument(
         OUTPUT_DIR_SHORT, OUTPUT_DIR_LONG, help=OUTPUT_DIR_HELP, type=str,
-        default=OUTPUT_DIR_DEFAULT
+        default=out_dir_default
         )
     parser.add_argument(
         SOURCE_DIR_SHORT, SOURCE_DIR_LONG, help=SOURCE_DIR_HELP, type=str,
-        default=SOURCE_DIR_DEFAULT
+        default=src_dir_default
         )
     parser.add_argument(
         NO_MAKEDIR_SHORT, NO_MAKEDIR_LONG, help=NO_MAKEDIR_HELP,
@@ -58,7 +63,7 @@ def create_argparser():
         )
     parser.add_argument(
         GS_SHORT, GS_LONG, help=GS_HELP,
-        type=str, default=GHOSTSCRIPT_BINARY_DEFAULT
+        type=str, default=gs_default_binary
         )
     parser.add_argument(
         SEND_SHORT, SEND_LONG, help=SEND_HELP,
diff --git a/pdfebc/main.py b/pdfebc/main.py
index d5a12e7..6162f76 100644
--- a/pdfebc/main.py
+++ b/pdfebc/main.py
@@ -6,6 +6,7 @@ Author: Simon Larsén
 import os
 import shutil
 import smtplib
+import sys
 from . import cli, core, utils
 
 AUTH_ERROR = """An authentication error has occured!
@@ -24,7 +25,14 @@ Please open an issue about this error at 'https://github.com/slarse/pdfebc/issue
 
 def main():
     """Run PDFEBC."""
-    parser = cli.create_argparser()
+    try:
+        parser = cli.create_argparser()
+    except utils.ConfigurationError as e:
+        print("FIRST TIME CONFIG NOT YET IMPLEMENTED")
+        print("FIX THE CONFIG FILE!")
+        print("EXITING!")
+        print(e)
+        sys.exit(1)
     args = parser.parse_args()
     if not args.nomakedir:
         os.makedirs(args.outdir)
diff --git a/pdfebc/utils.py b/pdfebc/utils.py
index 90b2ca9..d4d3fdd 100644
--- a/pdfebc/utils.py
+++ b/pdfebc/utils.py
@@ -1,20 +1,18 @@
 # -*- coding: utf-8 -*-
 """Module containing util functions for the pdfebc program.
 
-It uses Google's SMTP server for sending emails. If you wish to use another server, simply change
-the SMTP_SERVER variable to your preferred server.
+The SMTP server and port are configured in the config.cnf file.
 
 Requires a config file called 'email.cnf' in the user conf directory specified by appdirs. In the
-case of Arch Linux, this is '$HOME/.config/pdfebc/email.cnf', but this may vary with distributions.
+case of Arch Linux, this is '$HOME/.config/pdfebc/config.cnf', but this may vary with distributions.
 The config file should have the following format:
 
-    |[email]
-    |user = sender_email
-    |pass = password
-    |receiver = receiver_email
-
-All characters after the colon and whitespace (as much whitespace as you'd like) until
-EOL counts as the username/password.
+    |[EMAIL]
+    |user = <sender_email>
+    |pass = <password>
+    |receiver = <receiver_email>
+    |smtp_server = <smtp_server>
+    |smtp_port = <smtp_port>
 
 .. module:: utils
     :platform: Unix
@@ -29,22 +27,40 @@ from email.mime.text import MIMEText
 from email.mime.application import MIMEApplication
 from email.mime.multipart import MIMEMultipart
 import appdirs
+from . import cli
 
-CONFIG_PATH = os.path.join(appdirs.user_config_dir('pdfebc'), 'email.cnf')
-SECTION_KEY = "email"
+CONFIG_FILENAME = 'config.cnf'
+CONFIG_PATH = os.path.join(appdirs.user_config_dir('pdfebc'), CONFIG_FILENAME)
+EMAIL_SECTION_KEY = "EMAIL"
 PASSWORD_KEY = "pass"
 USER_KEY = "user"
-RECIEVER_KEY = "receiver"
-SMTP_SERVER = "smtp.gmail.com"
+RECEIVER_KEY = "receiver"
+DEFAULT_SMTP_SERVER = "smtp.gmail.com"
+DEFAULT_SMTP_PORT = 587
+SMTP_SERVER_KEY = "smtp_server"
+SMTP_PORT_KEY = "smtp_port"
+EMAIL_SECTION_KEYS = {USER_KEY, PASSWORD_KEY, RECEIVER_KEY, SMTP_SERVER_KEY, SMTP_PORT_KEY}
+DEFAULT_SECTION_KEY = "DEFAULTS"
+GS_DEFAULT_BINARY_KEY = "gs_binary"
+SRC_DEFAULT_DIR_KEY = "src"
+OUT_DEFAULT_DIR_KEY = "out"
+DEFAULT_SECTION_KEYS = {GS_DEFAULT_BINARY_KEY, SRC_DEFAULT_DIR_KEY, OUT_DEFAULT_DIR_KEY}
+SECTION_KEYS = {EMAIL_SECTION_KEY: EMAIL_SECTION_KEYS,
+                DEFAULT_SECTION_KEY: DEFAULT_SECTION_KEYS}
 
 SENDING_PRECONF = """Sending files ...
 From: {}
 To: {}
 SMTP Server: {}
+SMTP Port: {}
+
 Files:
 {}"""
 FILES_SENT = "Files successfully sent!"""
 
+class ConfigurationError(configparser.ParsingError):
+    pass
+
 def create_email_config(user, password, receiver):
     """Create an email config.
 
@@ -57,10 +73,34 @@ def create_email_config(user, password, receiver):
         configparser.ConfigParser: A ConfigParser
     """
     config = configparser.ConfigParser()
-    config[SECTION_KEY] = {
+    config[EMAIL_SECTION_KEY] = {
         USER_KEY: user,
         PASSWORD_KEY: password,
-        RECIEVER_KEY: receiver}
+        RECEIVER_KEY: receiver}
+    return config
+
+def create_config(sections, section_contents):
+    """Create a config file from the provided sections and key value pairs.
+
+    Args:
+        sections (List[str]): A list of section keys.
+        key_value_pairs (Dict[str, str]): A list of of dictionaries. Must be as long as
+        the list of sections. That is to say, if there are two sections, there should be two
+        dicts.
+    Returns:
+        configparser.ConfigParser: A ConfigParser.
+    Raises:
+        ValueError
+    """
+    sections_length, section_contents_length = len(sections), len(section_contents)
+    if sections_length != section_contents_length:
+        raise ValueError("Mismatch between argument lengths.\n"
+                         "len(sections) = {}\n"
+                         "len(section_contents) = {}"
+                         .format(sections_length, section_contents_length))
+    config = configparser.ConfigParser()
+    for section, section_content in zip(sections, section_contents):
+        config[section] = section_content
     return config
 
 def write_config(config, config_path=CONFIG_PATH):
@@ -75,15 +115,13 @@ def write_config(config, config_path=CONFIG_PATH):
     with open(config_path, 'w', encoding='utf-8') as f:
         config.write(f)
 
-def read_email_config(config_path=CONFIG_PATH):
-    """Read the email config file.
+def read_config(config_path=CONFIG_PATH):
+    """Read the config information from the config file.
 
     Args:
         config_path (str): Relative path to the email config file.
-
     Returns:
-        (str, str, str): User email, user password and receiver email.
-
+        configparser.ConfigParser: A ConfigParser with the config information.
     Raises:
         IOError
     """
@@ -91,10 +129,37 @@ def read_email_config(config_path=CONFIG_PATH):
         raise IOError("No config file found at %s" % config_path)
     config = configparser.ConfigParser()
     config.read(config_path)
-    user = try_get_conf(config, SECTION_KEY, USER_KEY)
-    password = try_get_conf(config, SECTION_KEY, PASSWORD_KEY)
-    receiver = try_get_conf(config, SECTION_KEY, RECIEVER_KEY)
-    return user, password, receiver
+    check_config(config)
+    return config
+
+def section_is_healthy(section, expected_keys):
+    """Check that the section contains all keys it should.
+
+    Args:
+        section (configparser.SectionProxy): A map-like object.
+        expected_keys (Iterable): A Set of keys that should be contained in the section.
+    Returns:
+        boolean: True if the section is healthy, false if not.
+    """
+    return set(section.keys()) == set(expected_keys)
+
+def check_config(config):
+    """Check that all sections of the config contain the keys that they should.
+
+    Args:
+        config (configparser.ConfigParser): A ConfigParser.
+    Raises:
+        ConfigurationError
+    """
+    for section, expected_section_keys in SECTION_KEYS.items():
+        try:
+            section_content = config[section]
+            if not section_is_healthy(section_content, expected_section_keys):
+                raise ConfigurationError("The {} section of the configuration file is badly formed!"
+                                         .format(section))
+        except KeyError as e:
+            raise ConfigurationError("Config file badly formed! Section {} is missing."
+                                     .format(section))
 
 def try_get_conf(config, section, attribute):
     """Try to parse an attribute of the config file.
@@ -103,37 +168,34 @@ def try_get_conf(config, section, attribute):
         config (configparser.ConfigParser): A ConfigParser.
         section (str): The section of the config file to get information from.
         attribute (str): The attribute of the section to fetch.
-
     Returns:
         str: The string corresponding to the section and attribute.
-
     Raises:
-        configparser.ParseError
+        ConfigurationError
     """
     try:
         return config[section][attribute]
     except KeyError:
-        raise configparser.ParsingError("""Config file badly formed!\n
-                Failed to get attribute '%s' from section '%s'!""" % (attribute, section))
+        raise ConfigurationError("Config file badly formed!\n"
+                                 "Failed to get attribute '{}' from section '{}'!"
+                                 .format(attribute, section))
 
-def send_with_attachments(user, password, receiver, subject, message, filepaths):
+def send_with_attachments(subject, message, filepaths, config):
     """Send an email from the user (a gmail) to the receiver.
 
     Args:
-        user (str): The sender's email address.
-        password (str): The password to the 'user' address.
-        receiver (str): The receiver's email address.
         subject (str): Subject of the email.
         message (str): A message.
         filepaths (list(str)): Filepaths to files to be attached.
+        config (configparser.ConfigParser): A ConfigParser.
     """
     email_ = MIMEMultipart()
     email_.attach(MIMEText(message))
     email_["Subject"] = subject
-    email_["From"] = user
-    email_["To"] = receiver
+    email_["From"] = try_get_conf(config, EMAIL_SECTION_KEY, USER_KEY)
+    email_["To"] = try_get_conf(config, EMAIL_SECTION_KEY, RECEIVER_KEY)
     attach_files(filepaths, email_)
-    send_email(user, password, email_)
+    send_email(email_, config)
 
 
 def attach_files(filepaths, email_):
@@ -150,15 +212,18 @@ def attach_files(filepaths, email_):
             part["Content-Disposition"] = 'attachment; filename="%s"' % base
             email_.attach(part)
 
-def send_email(user, password, email_):
+def send_email(email_, config):
     """Send an email.
 
     Args:
-        user (str): Sender's email address.
-        password (str): Password to sender's email.
         email_ (email.MIMEMultipart): The email to send.
+        config (configparser.ConfigParser): A ConfigParser.
     """
-    server = smtplib.SMTP(SMTP_SERVER, 587)
+    smtp_server = try_get_conf(config, EMAIL_SECTION_KEY, SMTP_SERVER_KEY)
+    smtp_port = int(try_get_conf(config, EMAIL_SECTION_KEY, SMTP_PORT_KEY))
+    user = try_get_conf(config, EMAIL_SECTION_KEY, USER_KEY)
+    password = try_get_conf(config, EMAIL_SECTION_KEY, PASSWORD_KEY)
+    server = smtplib.SMTP(smtp_server, smtp_port)
     server.starttls()
     server.login(user, password)
     server.send_message(email_)
@@ -170,12 +235,16 @@ def send_files_preconf(filepaths, config_path=CONFIG_PATH, status_callback=None)
     Args:
         filepaths (list(str)): A list of filepaths.
     """
-    user, password, receiver = read_email_config(config_path)
+    config = read_config(config_path)
     subject = "PDF files from pdfebc"
     message = ""
-    args = (user, receiver, SMTP_SERVER, '\n'.join(filepaths))
+    args = (try_get_conf(config, EMAIL_SECTION_KEY, USER_KEY),
+            try_get_conf(config, EMAIL_SECTION_KEY, RECEIVER_KEY),
+            try_get_conf(config, EMAIL_SECTION_KEY, SMTP_SERVER_KEY),
+            try_get_conf(config, EMAIL_SECTION_KEY, SMTP_PORT_KEY),
+            '\n'.join(filepaths))
     if_callable_call_with_formatted_string(status_callback, SENDING_PRECONF, *args)
-    send_with_attachments(user, password, receiver, subject, message, filepaths)
+    send_with_attachments(subject, message, filepaths, config)
     if_callable_call_with_formatted_string(status_callback, FILES_SENT)
 
 def valid_config_exists(config_path=CONFIG_PATH):
@@ -189,8 +258,8 @@ def valid_config_exists(config_path=CONFIG_PATH):
     """
     if (os.path.isfile(config_path)):
         try:
-            read_email_config(config_path)
-        except configparser.ParsingError:
+            read_config(config_path)
+        except ConfigurationError or IOError:
             return False
     else:
         return False


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index ff146e8..f35c7d9 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -17,9 +17,27 @@ class UtilsTest(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls):
-        cls.expected_user = "test_user"
-        cls.expected_password = "test_password"
-        cls.expected_receiver = "test_receiver"
+        cls.user = 'test_user'
+        cls.password = 'test_password'
+        cls.receiver = 'test_receiver'
+        cls.smtp_server = 'test_server'
+        cls.smtp_port = 999
+        cls.user_key = pdfebc.utils.USER_KEY
+        cls.password_key = pdfebc.utils.PASSWORD_KEY
+        cls.receiver_key = pdfebc.utils.RECEIVER_KEY
+        cls.smtp_server_key = pdfebc.utils.SMTP_SERVER_KEY
+        cls.smtp_port_key = pdfebc.utils.SMTP_PORT_KEY
+        cls.gs_binary_default_key = pdfebc.utils.GS_DEFAULT_BINARY_KEY
+        cls.out_dir_default_key = pdfebc.utils.OUT_DEFAULT_DIR_KEY
+        cls.src_dir_default_key = pdfebc.utils.SRC_DEFAULT_DIR_KEY
+        cls.email_section_key = pdfebc.utils.EMAIL_SECTION_KEY
+        cls.default_section_key = pdfebc.utils.DEFAULT_SECTION_KEY
+        cls.email_section_keys = {cls.user_key, cls.password_key, cls.receiver_key,
+                                  cls.smtp_server_key, cls.smtp_port_key}
+        cls.default_section_keys = {cls.gs_binary_default_key, cls.src_dir_default_key, cls.out_dir_default_key}
+        cls.section_keys = {cls.email_section_key: cls.email_section_keys,
+                            cls.default_section_key: cls.default_section_keys}
+
 
     @classmethod
     def setUp(cls):
@@ -30,12 +48,21 @@ class UtilsTest(unittest.TestCase):
                 encoding='utf-8', suffix=pdfebc.core.PDF_EXTENSION, mode='w', delete=False)
             cls.attachment_filenames.append(file.name)
             file.close()
-        cls.valid_config = pdfebc.utils.create_email_config(cls.expected_user,
-                                                            cls.expected_password,
-                                                            cls.expected_receiver)
+        cls.valid_config = configparser.ConfigParser()
+        cls.valid_config[pdfebc.utils.EMAIL_SECTION_KEY] = ({
+            cls.user_key: cls.user,
+            cls.password_key: cls.password,
+            cls.receiver_key: cls.receiver,
+            cls.smtp_server_key: cls.smtp_server,
+            cls.smtp_port_key: cls.smtp_port})
+        cls.valid_config[pdfebc.utils.DEFAULT_SECTION_KEY] = {
+            cls.gs_binary_default_key: pdfebc.cli.GHOSTSCRIPT_BINARY_DEFAULT,
+            cls.src_dir_default_key: pdfebc.cli.SOURCE_DIR_DEFAULT,
+            cls.out_dir_default_key: pdfebc.cli.OUTPUT_DIR_DEFAULT}
         cls.invalid_config = configparser.ConfigParser()
-        cls.invalid_config[pdfebc.utils.SECTION_KEY] = {pdfebc.utils.USER_KEY: cls.expected_user,
-                                                        pdfebc.utils.PASSWORD_KEY: cls.expected_password}
+        cls.invalid_config[pdfebc.utils.EMAIL_SECTION_KEY] = {
+            cls.user_key: cls.user,
+            cls.password_key: cls.password}
 
     @classmethod
     def tearDown(cls):
@@ -44,42 +71,73 @@ class UtilsTest(unittest.TestCase):
         for filename in cls.attachment_filenames:
             os.unlink(filename)
 
+    def test_create_config(self):
+        sections = self.section_keys.keys()
+        section_contents = [{
+            self.user_key: self.user,
+            self.password_key: self.password,
+            self.receiver_key: self.receiver,
+            self.smtp_server_key: pdfebc.utils.DEFAULT_SMTP_SERVER,
+            self.smtp_port_key: str(pdfebc.utils.DEFAULT_SMTP_PORT)},
+           {self.gs_binary_default_key: pdfebc.cli.GHOSTSCRIPT_BINARY_DEFAULT,
+            self.src_dir_default_key: pdfebc.cli.SOURCE_DIR_DEFAULT,
+            self.out_dir_default_key: pdfebc.cli.OUTPUT_DIR_DEFAULT}]
+        config = pdfebc.utils.create_config(sections, section_contents)
+        for section, section_content in zip(sections, section_contents):
+            config_section = config[section]
+            for section_content_key, section_content_value in section_content.items():
+                self.assertEqual(section_content_value, config_section[section_content_key])
+
+    def test_create_config_too_few_sections(self):
+        sections = ["EMAIL"]
+        section_contents = [{1: 2, 3: 4}, {1: 2}]
+        with self.assertRaises(ValueError):
+            pdfebc.utils.create_config(sections, section_contents)
+
     def test_write_valid_email_config(self):
         self.temp_config_file.close()
         pdfebc.utils.write_config(self.valid_config, self.temp_config_file.name)
         config = configparser.ConfigParser()
         with open(self.temp_config_file.name) as file:
             config.read_file(file)
-        section = config[pdfebc.utils.SECTION_KEY]
-        self.assertEqual(self.expected_user, section[pdfebc.utils.USER_KEY])
-        self.assertEqual(self.expected_password, section[pdfebc.utils.PASSWORD_KEY])
-        self.assertEqual(self.expected_receiver, section[pdfebc.utils.RECIEVER_KEY])
+        section = config[pdfebc.utils.EMAIL_SECTION_KEY]
+        self.assertEqual(self.user, section[self.user_key])
+        self.assertEqual(self.password, section[self.password_key])
+        self.assertEqual(self.receiver, section[self.receiver_key])
 
     def test_read_valid_email_config(self):
         self.valid_config.write(self.temp_config_file)
         self.temp_config_file.flush()
         self.temp_config_file.close()
-        actual_user, actual_password, actual_receiver = pdfebc.utils.read_email_config(self.temp_config_file.name)
-        self.assertEqual(self.expected_user, actual_user)
-        self.assertEqual(self.expected_password, actual_password)
-        self.assertEqual(self.expected_receiver, actual_receiver)
+        email_section = pdfebc.utils.read_config(self.temp_config_file.name)[
+            pdfebc.utils.EMAIL_SECTION_KEY]
+        actual_user = email_section[self.user_key]
+        actual_password = email_section[self.password_key]
+        actual_receiver = email_section[self.receiver_key]
+        actual_smtp_server = email_section[self.smtp_server_key]
+        actual_smtp_port = int(email_section[self.smtp_port_key])
+        self.assertEqual(self.user, actual_user)
+        self.assertEqual(self.password, actual_password)
+        self.assertEqual(self.receiver, actual_receiver)
+        self.assertEqual(self.smtp_server, actual_smtp_server)
+        self.assertEqual(self.smtp_port, actual_smtp_port)
 
-    def test_read_empty_email_config(self):
-        with self.assertRaises(configparser.ParsingError) as context:
-            pdfebc.utils.read_email_config(self.temp_config_file.name)
+    def test_read_empty_config(self):
+        with self.assertRaises(pdfebc.utils.ConfigurationError) as context:
+            pdfebc.utils.read_config(self.temp_config_file.name)
 
-    def test_read_email_config_no_file(self):
+    def test_read_config_no_file(self):
         with tempfile.NamedTemporaryFile() as tmp:
             config_path = tmp.name
         with self.assertRaises(IOError) as context:
-            pdfebc.utils.read_email_config(config_path)
+            pdfebc.utils.read_config(config_path)
 
-    def test_read_email_config_without_receiver(self):
+    def test_read_config_with_only_user_and_password(self):
         self.invalid_config.write(self.temp_config_file)
         self.temp_config_file.flush()
         self.temp_config_file.close()
-        with self.assertRaises(configparser.ParsingError) as context:
-            pdfebc.utils.read_email_config(self.temp_config_file.name)
+        with self.assertRaises(pdfebc.utils.ConfigurationError) as context:
+            pdfebc.utils.read_config(self.temp_config_file.name)
 
     def test_attach_valid_files(self):
         email_ = MIMEMultipart()
@@ -94,43 +152,43 @@ class UtilsTest(unittest.TestCase):
 
     @patch('smtplib.SMTP')
     def test_send_valid_email(self, mock_smtp):
-        mock_smtp_instance = mock_smtp()
-        user, password, receiver = "test_user", "test_password", "test_receiver"
         subject = "Test e-mail"
         email_ = MIMEMultipart()
-        email_['From'] = user
-        email_['To'] = receiver
+        email_['From'] = self.user
+        email_['To'] = self.receiver
         email_['Subject'] = subject
-        pdfebc.utils.send_email(user, password, email_)
+        pdfebc.utils.send_email(email_, self.valid_config)
+        mock_smtp.assert_called_once_with(self.smtp_server, self.smtp_port)
+        mock_smtp_instance = mock_smtp()
         mock_smtp_instance.starttls.assert_called_once()
-        mock_smtp_instance.login.assert_called_once_with(user, password)
+        mock_smtp_instance.login.assert_called_once_with(self.user, self.password)
         mock_smtp_instance.send_message.assert_called_once_with(email_)
         mock_smtp_instance.quit.assert_called_once()
 
     @patch('smtplib.SMTP')
     def test_send_valid_email_with_attachments(self, mock_smtp):
-        mock_smtp_instance = mock_smtp()
         subject = "Test e-mail"
         message = "Test e-mail body"
-        pdfebc.utils.send_with_attachments(self.expected_user, self.expected_password,
-                                           self.expected_receiver, subject, message,
-                                           self.attachment_filenames)
+        pdfebc.utils.send_with_attachments(subject, message, self.attachment_filenames,
+                                           self.valid_config)
+        mock_smtp.assert_called_once_with(self.smtp_server, self.smtp_port)
+        mock_smtp_instance = mock_smtp()
         mock_smtp_instance.starttls.assert_called_once()
-        mock_smtp_instance.login.assert_called_once_with(self.expected_user, self.expected_password)
+        mock_smtp_instance.login.assert_called_once_with(self.user, self.password)
         mock_smtp_instance.send_message.assert_called_once()
         mock_smtp_instance.quit.assert_called_once()
 
     def test_create_email_config(self):
-        section_key = pdfebc.utils.SECTION_KEY
-        user_key = pdfebc.utils.USER_KEY
-        password_key = pdfebc.utils.PASSWORD_KEY
-        receiver_key = pdfebc.utils.RECIEVER_KEY
+        section_key = pdfebc.utils.EMAIL_SECTION_KEY
+        user_key = self.user_key
+        password_key = self.password_key
+        receiver_key = self.receiver_key
         actual_user = self.valid_config[section_key][user_key]
         actual_password = self.valid_config[section_key][password_key]
         actual_receiver = self.valid_config[section_key][receiver_key]
-        self.assertEqual(self.expected_user, actual_user)
-        self.assertEqual(self.expected_password, actual_password)
-        self.assertEqual(self.expected_receiver, actual_receiver)
+        self.assertEqual(self.user, actual_user)
+        self.assertEqual(self.password, actual_password)
+        self.assertEqual(self.receiver, actual_receiver)
 
     def test_valid_config_exists_no_config(self):
         with tempfile.NamedTemporaryFile() as file:
@@ -143,7 +201,7 @@ class UtilsTest(unittest.TestCase):
         config_path = self.temp_config_file.name
         self.assertTrue(pdfebc.utils.valid_config_exists(config_path))
 
-    def test_valid_config_exists_with_config_without_receiver(self):
+    def test_valid_config_exists_with_invalid_config(self):
         self.invalid_config.write(self.temp_config_file)
         self.temp_config_file.close()
         config_path = self.temp_config_file.name
@@ -201,12 +259,12 @@ class UtilsTest(unittest.TestCase):
         pdfebc.utils.send_files_preconf(self.attachment_filenames, config_path=self.temp_config_file.name,
                                         status_callback=mock_status_callback)
         mock_smtp_instance.starttls.assert_called_once()
-        mock_smtp_instance.login.assert_called_once_with(self.expected_user, self.expected_password)
+        mock_smtp_instance.login.assert_called_once_with(self.user, self.password)
         mock_smtp_instance.send_message.assert_called_once()
         mock_smtp_instance.quit.assert_called_once()
         expected_send_message = pdfebc.utils.SENDING_PRECONF.format(
-            self.expected_user, self.expected_receiver,
-            pdfebc.utils.SMTP_SERVER, '\n'.join(self.attachment_filenames))
+            self.user, self.receiver,
+            self.smtp_server, self.smtp_port, '\n'.join(self.attachment_filenames))
         expected_sent_message = pdfebc.utils.FILES_SENT
         mock_status_callback.assert_any_call(expected_send_message)
         mock_status_callback.assert_any_call(expected_sent_message)