Skip to content

Commit 065941a

Browse files
committed
emails: add wm api backend
Bug: T412427
1 parent 0201ab9 commit 065941a

File tree

2 files changed

+175
-0
lines changed

2 files changed

+175
-0
lines changed

TWLight/emails/backends/__init__.py

Whitespace-only changes.
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
"""
2+
Email backend that POSTs messages to the MediaWiki Emailuser endpoint.
3+
see: https://www.mediawiki.org/wiki/API:Emailuser
4+
"""
5+
import logging
6+
import os
7+
from requests import Session
8+
from requests.structures import CaseInsensitiveDict
9+
from sys import stdout
10+
import threading
11+
12+
from django.conf import settings
13+
from django.core.mail.backends.base import BaseEmailBackend
14+
15+
from TWLight.users.models import Editor
16+
17+
logger = logging.getLogger("django")
18+
19+
20+
class EmailBackend(BaseEmailBackend):
21+
def __init__(
22+
self,
23+
url=None,
24+
username=None,
25+
password=None,
26+
timeout=None,
27+
rate_limit=None,
28+
stream=False,
29+
fail_silently=False,
30+
**kwargs,
31+
):
32+
super().__init__(fail_silently=fail_silently)
33+
self.url = settings.MW_API_URL if url is None else url
34+
self.maxlag = 5
35+
self.headers = CaseInsensitiveDict()
36+
self.headers["User-Agent"] = "twl mw.api.emailuser.py/0.0.1"
37+
self.timeout = settings.MW_API_TIMEOUT if timeout is None else timeout
38+
self.username = settings.MW_API_EMAIL_USER if username is None else username
39+
self.password = settings.MW_API_EMAIL_PASSWORD if password is None else password
40+
self.stream = stream or stdout
41+
self.email_token = None
42+
self.session = None
43+
self._lock = threading.RLock()
44+
45+
def open(self):
46+
"""
47+
Ensure an open session to the API server. Return whether or not a
48+
new session was required (True or False) or None if an exception
49+
passed silently.
50+
"""
51+
if self.session:
52+
# Nothing to do if the session exists
53+
return False
54+
55+
try:
56+
# GET request to fetch login token
57+
login_token_params = {
58+
"action": "query",
59+
"meta": "tokens",
60+
"type": "login",
61+
"format": "json",
62+
}
63+
logger.info("Getting login token...")
64+
session = Session()
65+
response_login_token = session.get(url=url, params=login_token_params)
66+
if response_login_token.status_code != 200:
67+
raise Exception(
68+
"There was an error in the request for obtaining the login token."
69+
)
70+
login_token_data = response_login_token.json()
71+
login_token = login_token_data["query"]["tokens"]["logintoken"]
72+
if not login_token:
73+
raise Exception("There was an error obtaining the login token.")
74+
75+
# POST request to log in. Use of main account for login is not
76+
# supported. Obtain credentials via Special:BotPasswords
77+
# (https://www.mediawiki.org/wiki/Special:BotPasswords) for lgname & lgpassword
78+
login_params = {
79+
"action": "login",
80+
"lgname": self.username,
81+
"lgpassword": self.password,
82+
"lgtoken": login_token,
83+
"format": "json",
84+
}
85+
logger.info("Signing in...")
86+
login_response = session.post(self.url, data=login_params)
87+
if login_response.status_code != 200:
88+
raise Exception("There was an error in the request for the login.")
89+
90+
# GET request to fetch Email token
91+
email_token_params = {"action": "query", "meta": "tokens", "format": "json"}
92+
93+
logger.info("Getting email token...")
94+
email_token_response = self.session.get(url=url, params=email_token_params)
95+
email_token_data = email_token_response.json()
96+
97+
# Assign the session and email token
98+
self.email_token = email_token_data["query"]["tokens"]["csrftoken"]
99+
self.session = session
100+
return True
101+
except:
102+
if not self.fail_silently:
103+
raise
104+
105+
def close(self):
106+
"""Unset the session."""
107+
self.session = None
108+
109+
def send_messages(self, email_messages):
110+
"""
111+
Send one or more EmailMessage objects and return the number of email
112+
messages sent.
113+
"""
114+
if not email_messages:
115+
return 0
116+
with self._lock:
117+
new_session_created = self.open()
118+
if not self.session or new_session_created is None:
119+
# We failed silently on open().
120+
# Trying to send would be pointless.
121+
return 0
122+
num_sent = 0
123+
for message in email_messages:
124+
sent = self._send(message)
125+
if sent:
126+
num_sent += 1
127+
if new_session_created:
128+
self.close()
129+
return num_sent
130+
131+
def _send(self, email_message):
132+
"""A helper method that does the actual sending."""
133+
if not email_message.recipients():
134+
return False
135+
136+
try:
137+
for recipient in email_message.recipients():
138+
# lookup the target editor from the email address
139+
target = Editor.objects.values_list("wp_username", flat=True).get(
140+
user__email=recipient
141+
)
142+
143+
# GET request to check if user is emailable
144+
emailable_params = {
145+
"action": "query",
146+
"list": "users",
147+
"ususers": target,
148+
"usprop": "emailable",
149+
"format": "json",
150+
}
151+
152+
logger.info("Checking if user is emailable...")
153+
emailable_response = session.get(url, data=emailable_params)
154+
emailable_data = emailable_response.json()
155+
emailable = "emailable" in emailable_data["query"]["users"][0]
156+
if not emailable:
157+
continue
158+
159+
# POST request to send an email
160+
email_params = {
161+
"action": "emailuser",
162+
"target": target,
163+
"subject": email_message.subject,
164+
"text": email_message.body,
165+
"token": self.email_token,
166+
"format": "json",
167+
}
168+
169+
logger.info("Sending email...")
170+
email_response = session.post(url, data=email_params)
171+
except:
172+
if not self.fail_silently:
173+
raise
174+
return False
175+
return True

0 commit comments

Comments
 (0)