Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions browser/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def interpret_bypass_queue(mailbox, mode, extra_info):

# create a string buffer to store stdout
user_std_out = StringIO()
user_property_log = StringIO()
with sandbox_helpers.override_print(user_std_out) as fakeprint:
code = extra_info['code']
message_schemas = MessageSchema.objects.filter(id=extra_info['msg-id'])
Expand All @@ -46,6 +47,9 @@ def interpret_bypass_queue(mailbox, mode, extra_info):
user_environ['new_message'] = new_message
mailbox._imap_client.select_folder(message_schema.folder.name)

# clear the property log at the last possible moment
user_property_log.truncate(0)
mailbox._imap_client.user_property_log = user_property_log
# execute the user's code
if "on_message" in code:
exec(code + "\non_message(new_message)", user_environ)
Expand All @@ -61,6 +65,8 @@ def interpret_bypass_queue(mailbox, mode, extra_info):
elif "on_deadline" in code:
exec(code + "\non_deadline(new_message)", user_environ)

mailbox._imap_client.user_property_log = None

except Exception:
# Get error message for users if occurs
# print out error messages for user
Expand All @@ -69,6 +75,7 @@ def interpret_bypass_queue(mailbox, mode, extra_info):
fakeprint(sandbox_helpers.get_error_as_string_for_user())
finally:
msg_log["log"] += user_std_out.getvalue()
msg_log["log"] += user_property_log.getvalue()
# msg_log["log"] = "%s\n%s" % (user_std_out.getvalue(), msg_log["log"])
res['appended_log'][message_schema.id] = msg_log

Expand Down
19 changes: 10 additions & 9 deletions engine/models/contact.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from schema.youps import ContactSchema, MessageSchema # noqa: F401 ignore unused we use it for typing
from django.db.models import Q
import logging
from engine.models.helpers import CustomProperty

logger = logging.getLogger('youps') # type: logging.Logger

Expand Down Expand Up @@ -32,7 +33,7 @@ def __eq__(self, other):

return False

@property
@CustomProperty
def email(self):
# type: () -> t.AnyStr
"""Get the email address associated with this contact
Expand All @@ -42,7 +43,7 @@ def email(self):
"""
return self._schema.email

@property
@CustomProperty
def aliases(self):
# type: () -> t.List[t.AnyStr]
"""Get all the names associated with this contact
Expand All @@ -52,7 +53,7 @@ def aliases(self):
"""
return self._schema.aliases.all().values_list('name', flat=True)

@property
@CustomProperty
def name(self):
# type: () -> t.AnyStr
"""Get the name associated with this contact
Expand All @@ -63,7 +64,7 @@ def name(self):
# simply returns the most common alias
return self._schema.aliases.order_by('-count').first().name

@property
@CustomProperty
def organization(self):
# type: () -> t.AnyStr
"""Get the organization of this contact
Expand All @@ -73,7 +74,7 @@ def organization(self):
"""
return self._schema.organization

@property
@CustomProperty
def geolocation(self):
# type: () -> t.AnyStr
"""Get the location of this contact
Expand All @@ -83,7 +84,7 @@ def geolocation(self):
"""
return self._schema.geolocation

@property
@CustomProperty
def messages_to(self):
# type: () -> t.List[Message]
"""Get the Messages which are to this contact
Expand All @@ -94,7 +95,7 @@ def messages_to(self):
from engine.models.message import Message
return [Message(message_schema, self._imap_client) for message_schema in self._schema.to_messages.all()]

@property
@CustomProperty
def messages_from(self):
# type: () -> t.List[Message]
"""Get the Messages which are from this contact
Expand All @@ -105,7 +106,7 @@ def messages_from(self):
from engine.models.message import Message
return [Message(message_schema, self._imap_client) for message_schema in self._schema.from_messages.all()]

@property
@CustomProperty
def messages_bcc(self):
# type: () -> t.List[Message]
"""Get the Messages which are bcc this contact
Expand All @@ -116,7 +117,7 @@ def messages_bcc(self):
from engine.models.message import Message
return [Message(message_schema, self._imap_client) for message_schema in self._schema.bcc_messages.all()]

@property
@CustomProperty
def messages_cc(self):
# type: () -> t.List[Message]
"""Get the Messages which are cc this contact
Expand Down
29 changes: 14 additions & 15 deletions engine/models/folder.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
import heapq
import logging
import typing as t # noqa: F401 ignore unused we use it for typing
import re
from datetime import datetime
from email.header import decode_header
from email.utils import parseaddr, getaddresses
from string import whitespace
from email.utils import getaddresses
from itertools import chain

import chardet
Expand All @@ -25,10 +23,11 @@
NewMessageDataScheduled,
RemovedFlagsData)
from engine.models.message import Message
from engine.utils import normalize_msg_id, folding_ws_regex, encoded_word_string_regex, header_comment_regex
from engine.utils import normalize_msg_id, FOLDING_WS_RE, ENCODED_WORD_STRING_RE, HEADER_COMMENT_RE
from schema.youps import ( # noqa: F401 ignore unused we use it for typing
BaseMessage, ContactSchema, ContactAlias, FolderSchema, ImapAccount,
MessageSchema, ThreadSchema)
from engine.models.helpers import CustomProperty

logger = logging.getLogger('youps') # type: logging.Logger

Expand All @@ -55,7 +54,7 @@ def __eq__(self, other):
return other == self.name
return False

@property
@CustomProperty
def _uid_next(self):
# type: () -> int
return self._schema.uid_next
Expand All @@ -66,7 +65,7 @@ def _uid_next(self, value):
self._schema.uid_next = value
self._schema.save()

@property
@CustomProperty
def _uid_validity(self):
# type: () -> int
return self._schema.uid_validity
Expand All @@ -77,7 +76,7 @@ def _uid_validity(self, value):
self._schema.uid_validity = value
self._schema.save()

@property
@CustomProperty
def _highest_mod_seq(self):
# type: () -> int
return self._schema.highest_mod_seq
Expand All @@ -88,7 +87,7 @@ def _highest_mod_seq(self, value):
self._schema.highest_mod_seq = value
self._schema.save()

@property
@CustomProperty
def name(self):
# type: () -> str
return self._schema.name
Expand All @@ -99,7 +98,7 @@ def name(self, value):
self._schema.name = value
self._schema.save()

@property
@CustomProperty
def _last_seen_uid(self):
# type: () -> int
return self._schema.last_seen_uid
Expand All @@ -110,7 +109,7 @@ def _last_seen_uid(self, value):
self._schema.last_seen_uid = value
self._schema.save()

@property
@CustomProperty
def _is_selectable(self):
# type: () -> bool
return self._schema.is_selectable
Expand All @@ -121,7 +120,7 @@ def _is_selectable(self, value):
self._schema.is_selectable = value
self._schema.save()

@property
@CustomProperty
def _imap_account(self):
# type: () -> ImapAccount
return self._schema.imap_account
Expand Down Expand Up @@ -531,11 +530,11 @@ def _parse_email_header(self, header):
return None

# replace instance of folding white space with nothing
header = folding_ws_regex.sub('', header)
header = FOLDING_WS_RE.sub('', header)
for field in header.split('\r\n'):
# header can have multiple encoded parts
# we can remove this in python3 but its a bug in python2
parts = chain.from_iterable(decode_header(f) for f in filter(None, encoded_word_string_regex.split(field)))
parts = chain.from_iterable(decode_header(f) for f in filter(None, ENCODED_WORD_STRING_RE.split(field)))
combined_parts = u""
for part in parts:
text, encoding = part[0], part[1]
Expand Down Expand Up @@ -567,8 +566,8 @@ def _parse_email_header(self, header):
v = fields[k]
# nested comments cannot be queried with regex
# this hack removes them from the inside out
while header_comment_regex.search(v):
v = header_comment_regex.sub('', v)
while HEADER_COMMENT_RE.search(v):
v = HEADER_COMMENT_RE.sub('', v)
fields[k] = v
return fields

Expand Down
3 changes: 3 additions & 0 deletions engine/models/helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from custom_property import CustomProperty

__all__ = ["CustomProperty"]
131 changes: 131 additions & 0 deletions engine/models/helpers/custom_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
from __future__ import print_function
import typing as t

import logging


def _get_class_name(obj):
# type: (object) -> str
"""Get the class name from an object

Returns:
str: the name of the class which represents the object
"""
return obj.__class__.__name__


def _get_method_name(prop):
# type: (t.Callable) -> str
"""Get the name of a method

Args:
prop (t.Callable): a method on an object

Returns:
str: the name which represents the method
"""
return prop.__name__


def _get_logger():
# type: () -> logging.Logger
return logging.getLogger('youps')


def _log_info(obj, info):
# type: (t.AnyStr) -> None
assert obj._imap_client is not None
if hasattr(obj._imap_client, 'user_property_log') and \
obj._imap_client.user_property_log is not None and \
hasattr(obj._imap_client, "nested_log") and \
obj._imap_client.nested_log is False:
_get_logger().critical("HEEEEEEEEEEERE")
user_property_log = obj._imap_client.user_property_log
user_property_log.write(info)
user_property_log.write(u"\n")
Copy link
Member

Choose a reason for hiding this comment

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

So we are just appending logs in one string object. Can we make it more parse-able? Can we include additional information which line of user's code this property log generated from? For example,

def on_message(my_message):
    if '😎' in my_message.subject and 'Luke Murray' == my_message.sender:
        my_message.deadline = datetime.now()

this will be

{ 2: [{'type': 'get', 'log': 'Message.subject	Testing Email 😎Yee'}, {'type': 'get', 'log': 'Message.sender [email protected]'}]
  3: [{'type': 'set', 'log': 'Message.deadline	None -> 2019-05-25 17:08:53.215517'}]



def _set_in_log(obj, value):
# if hasattr(obj._imap_client, 'user_property_log') and obj._imap_client.user_property_log is not None:
obj._imap_client.nested_log = value

def _is_nested(obj):
# if hasattr(obj._imap_client, 'user_property_log') and obj._imap_client.user_property_log is not None:
if hasattr(obj._imap_client, 'nested_log'):
return obj._imap_client.nested_log
return False

class CustomProperty(object):
"Emulate PyProperty_Type() in Objects/descrobject.c"
Copy link
Member

Choose a reason for hiding this comment

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

Is this a comment?


def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc

def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")

if _is_nested(obj):
return self.fget(obj)

_set_in_log(obj, True)

# value we are getting or wrapping around
value = self.fget(obj)

# name of the class we are a property on
class_name = _get_class_name(obj)
# name of the property we're wrapping around
property_name = _get_method_name(self.fget)

info_string = u"get {c}.{p}\t{v}".format(
c=class_name, p=property_name, v=value)
_set_in_log(obj, False)

if not property_name.startswith("_"):
Copy link
Member

Choose a reason for hiding this comment

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

Can we move this if statement right after property_name = _get_method_name(self.fget) so we can do early-exit?

_log_info(obj, info_string)

return value

def __set__(self, obj, new_value):
if self.fset is None:
raise AttributeError("can't set attribute")
if _is_nested(obj):
self.fset(obj, new_value)
return
# TODO doesn't really make sense normally but we want to fget for logging
if self.fget is None:
raise AttributeError("unreadable attribute")

_set_in_log(obj, True)
curr_value = self.fget(obj)
class_name = _get_class_name(obj)
property_name = _get_method_name(self.fset)
info_string = u"set {c}.{p}\t{v} -> {nv}".format(
c=class_name, p=property_name, v=curr_value, nv=new_value)
self.fset(obj, new_value)
_set_in_log(obj, False)

if not property_name.startswith("_"):
Copy link
Member

Choose a reason for hiding this comment

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

repeating: Can we move this if statement right after property_name = _get_method_name(self.fget) so we can do early-exit?

_log_info(obj, info_string)

def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)

def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.__doc__)

def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.__doc__)

def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__)
Loading