-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmain.py
285 lines (227 loc) · 9.16 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
import os
import sys
import ssl
import json
import email
import random
import getpass
import smtplib
import argparse
import email.utils
import email.policy
from email.message import EmailMessage
class SecretSanta:
name: str
email: str
def __init__(self, name: str, email: str) -> None:
self.name = name
self.email = email
def __str__(self) -> str:
return f"Name: {self.name}, Email: {self.email}"
def __eq__(self, __o: object) -> bool:
if isinstance(__o, self.__class__):
return __o.name == self.name and __o.email == self.email
return False
def __hash__(self):
return hash((self.name, self.email))
class Sender:
address: str # address of the sender server
email: str # email address, from which messages are being send
port: int # port, on which the server should listen
subject: str # subject of the email
body: str # body of the email
def __init__(self, address: str, email: str, port: int, subject: str, body: str) -> None:
self.address = address
self.email = email
self.port = port
self.subject = subject
self.body = body
def __str__(self) -> str:
return f"Email: {self.email}, Server Address: {self.email}, Port: {self.port}"
def __eq__(self, __o: object) -> bool:
if isinstance(__o, self.__class__):
return __o.address == self.address and __o.email == self.email and __o.port == self.port
return False
def __hash__(self):
return hash((self.address, self.email, self.port))
def setup_argparse() -> str:
"""
Configures argparse to accept path to files and returns the path.
:return: path to the config file
"""
def is_file(path: str) -> str:
"""
Checks weather a given string is a valid path.
:param path: path to a file
:return: path if it is valid
"""
if os.path.exists(path) and os.path.isfile(path):
return path
else:
raise Exception("Provided path doesn't exists")
# setup argparse and get path
parser = argparse.ArgumentParser(
description="a script, which draws for each user a secret santas and informs the santa which user it drew")
parser.add_argument(
"config_path",
type=is_file,
help="Path to the config json file, which stores the santas")
args = parser.parse_args()
path = args.config_path
return path
def load_config(path: str) -> dict[str, all]:
"""
Loads a config file which must be given by the first argument,
reads the file and parses it (must be json format).
:return: dict containing the users
"""
try:
# read provided file
with open(path, "r") as f:
raw = f.read()
except Exception as e:
raise Exception(f"Unable to open and read {path}.")
try:
# parse to dict and interpret as json
parsed = json.loads(raw)
except Exception as e:
raise Exception("The provided json file is not well formatted. Please see the README.md for more information.")
# interpret as json
return parsed
def extract_config(config: dict) -> tuple[Sender, list[SecretSanta]]:
"""
Extracts the sender and the secret santas from the parsed file.
Terminates if the input is invalid.
:param config: parsed file
:return: tuple containing a Sender object and a list of SecretSanta
"""
try:
# extract sender info and save as object in memory
sender = Sender(
address=config["sender"]["address"],
email=config["sender"]["email"],
port=config["sender"]["port"],
subject=config["sender"]["subject"],
body=config["sender"]["body"]
)
# extract santas
santas = []
for s in config["santas"]:
santa = SecretSanta(
s["name"],
s["email"]
)
santas.append(santa)
except Exception as e:
raise Exception("The provided json file is not well formatted. Please see the README.md for more information.")
# the minimal number of santas is two
if len(santas) <= 1:
raise Exception(f"Only {len(santas)} santa has been provided. "
"At least two santas have to play the game (but it starts to make sense with 3+ santas)")
# checking, that the emails are unique and printing warnings, if at least one name is unique
for i in range(len(santas)):
for j in range(len(santas)):
# skip entry's, where entries are equal
if i == j:
continue
# check weather if emails is equal
if santas[i].email == santas[j].email:
raise Exception(f"The email {santas[i].email} has been referenced twice.")
# check weather if usernames is equal
if santas[i].name == santas[j].name:
print(f"WARNING: The username \"{santas[i].name}\" has been used multiple times."
f"Please make sure, that none gets confused.")
return sender, santas
def shuffle_santas(santas: list) -> dict[SecretSanta, SecretSanta]:
"""
Shuffles a list of santas, so that a santa did not draw itself.
:param santas: list of santas
:return: dictionary, where a santa references another santa
"""
# shuffle santas
random.shuffle(santas)
# copy santas
assigned_santas = santas.copy()
s = assigned_santas.pop(0)
assigned_santas.append(s)
# convert assigned santas and other santas to dict
santa_dict = {}
for i in range(len(santas)):
santa_dict[santas[i]] = assigned_santas[i]
return santa_dict
def send_santa_invitations(sender: Sender, password: str, santas: dict[SecretSanta, SecretSanta]) -> None:
"""
Sends an email to every santa, with the name of the santa it drew.
:param sender: sender object used for creating the server
:param password: password used for authenticating the server
:param santas: dict of santas, with reference which user it drew
:return: None
"""
def construct_message(santa: SecretSanta, recipient: SecretSanta, sender: Sender) -> EmailMessage:
"""
Constructs a message to the recipient from the sender.
The sender email address in the Header is changed to fit the theme.
:param santa: santa, which drew the recipient
:param recipient: recipient, which is being gifted by the santa
:param sender: Sender object
:return: Message object with RFC 5322 formatted header
"""
def replace_reference(input: str, santa: SecretSanta, recipient: SecretSanta):
"""
Replaces from an input string all occurrence of special codes (see README.md)
:param input: the input string
:param santa: Secret Santa, who has to gift someone
:param recipient: Secret Santa, who is being gifted
:return: input string, with the special codes being replaced with the according variables
"""
output = input.replace("{santa.name}", santa.name)\
.replace("{santa.email}", santa.email)\
.replace("{recipient.name}", recipient.name)\
.replace("{recipient.email}", recipient.email)
return output
subject = replace_reference(sender.subject, santa, recipient)
body = replace_reference(sender.body, santa, recipient)
message = EmailMessage(email.policy.SMTP)
message["To"] = santa.email
message["From"] = sender.email
message["Subject"] = subject
message["Date"] = email.utils.formatdate(localtime=True)
message["Message-ID"] = email.utils.make_msgid()
message.set_content(body)
return message
try:
# setup smtp for sending mails
with smtplib.SMTP_SSL(sender.address, sender.port) as server:
# login to server
server.login(sender.email, password)
# send all mails to every santa
for santa in santas:
print(f"Sending email to {santa.name}: {santa.email} ...")
msg = construct_message(santa, santas[santa], sender)
server.send_message(msg)
except Exception as e:
raise Exception("Error while sending at least one email.", e)
def main():
try:
print("Getting Arguments...")
config_path = setup_argparse()
print("Done\n")
print("Parsing configuration...")
config = load_config(config_path)
print("Done\n")
print("Extracting Santas...")
sender, santas = extract_config(config)
print("Done\n")
print("Calculating Santas...")
santas_assigned = shuffle_santas(santas)
print("Done\n")
# read the credentials for sending the emails
password = getpass.getpass("Please enter the password for your email address: ")
print()
print("Sending emails to the santas...")
send_santa_invitations(sender, password, santas_assigned)
print("Done\n")
except Exception as e:
print("ERROR:", e, file=sys.stderr)
if __name__ == "__main__":
main()