Compare commits

..

1 Commits

Author SHA1 Message Date
B. Petersen
c63fbf199a display failed messages 2019-11-02 00:34:14 +01:00
26 changed files with 250 additions and 521 deletions

View File

@@ -1,19 +1,5 @@
# Changelog
## untagged 1.0.0-beta.7
- fix location-streaming #782
- fix display of messages that could not be decrypted #785
- fix smtp MAILER-DAEMON bug #786
- fix a logging of durations #783
- add more error logging #779
- do not panic on some bad utf-8 mime #776
## 1.0.0-beta.6
- fix chatlist.get_msg_id to return id, instead of wrongly erroring

View File

@@ -2951,32 +2951,6 @@ int64_t dc_msg_get_sort_timestamp (const dc_msg_t* msg);
*/
char* dc_msg_get_text (const dc_msg_t* msg);
/**
* Get the text of the message.
* If there is no text associated with the message, an empty string is returned.
* NULL is never returned.
*
* Same as dc_msg_get_text, but isn't truncated
*
* To get information about the message and more/raw text, use dc_get_msg_info().
*
* @memberof dc_msg_t
* @param msg The message object.
* @return Message text. The result must be released using dc_str_unref(). Never returns NULL.
*/
char* dc_msg_get_full_text (const dc_msg_t* msg);
/**
* Check if a message is truncated.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return 1=message is truncated, 0=message not truncated.
*/
int dc_msg_is_truncated (const dc_msg_t* msg);
// define DC_MSG_TRUNCATE_THRESHOLD
/**
* Find out full path, file name and extension of the file associated with a

View File

@@ -2420,26 +2420,7 @@ pub unsafe extern "C" fn dc_msg_get_text(msg: *mut dc_msg_t) -> *mut libc::c_cha
return dc_strdup(ptr::null());
}
let ffi_msg = &*msg;
ffi_msg.message.get_text(false).unwrap_or_default().strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_full_text(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_full_text()");
return dc_strdup(ptr::null());
}
let ffi_msg = &*msg;
ffi_msg.message.get_text(true).unwrap_or_default().strdup()
}
#[no_mangle]
pub unsafe extern "C" fn is_truncated(msg: *mut dc_msg_t) -> libc::c_int {
if msg.is_null() {
return 0
}
let ffi_msg = &*msg;
ffi_msg.message.is_truncated().into()
ffi_msg.message.get_text().unwrap_or_default().strdup()
}
#[no_mangle]

View File

@@ -189,7 +189,7 @@ unsafe fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
_ => "",
};
let temp2 = dc_timestamp_to_str(msg.get_timestamp());
let msgtext = msg.get_text(false);
let msgtext = msg.get_text();
info!(
context,
"{}#{}{}{}: {} (Contact#{}): {} {}{}{}{}{} [{}]",
@@ -353,7 +353,6 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
configure\n\
connect\n\
disconnect\n\
interrupt\n\
maybenetwork\n\
housekeeping\n\
help imex (Import/Export)\n\
@@ -494,9 +493,6 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E
"info" => {
println!("{:#?}", context.get_info());
}
"interrupt" => {
interrupt_imap_idle(context);
}
"maybenetwork" => {
maybe_network(context);
}

View File

@@ -1,7 +0,0 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 03cab93c6d1f3a8245f63cf84dacb307944294fe6333c1e38f078a6600659c7a # shrinks to data = "a\t0aA\ta\t0 \ta\t0 \ta a\t\ta A\tAA0a0a 0\t a\t aA \t a\t A0\t AAa\taA0\taAAaA\t0\taa0a\ta Aa Aaaa A0A\t a aA 0\t A\t0\t0\t\t\t\t\t\tA \t\t a\tA Aa aAA0A0AA0aaA A\t\t aa0\ta\t \tAa\taA\t00 AA A a\tA a aAAa \t 00\t0 \t\t a A 0\t\t\t aAA Aa \taAAAA0a A0A\t\t1\\E$\t$R\tc\t^=\t\"\tQ<Uk\t\t>A\t\t&\t}v&\tM^\'|\tW5?dn\t\t+\t\tP\te`\t\t>:Brlq--?\t$#Q\tK=zJ\tb\"9*.\"\t`\tF&T*\tBs,\tg\'*\\\t:\t?l$\t\t|A\"HR:Hk\t\\KkV\t\t{=&!^e%:|_*0wV\t[$`\t:\t$%f\t\t[!\"Y. \tP\t\th\'?\'/?%:++NfQo#@\"+?\t(\\??{\t\'\'$Dzj\t0.?{s4?&?Y=/yj]Z=\t4n\t?Ja\"\t{I\t$\t;I}_8V\t&\t?N\'\tI2/\t9.\tFT%9%`\'\tz\to7Y\t|AXP&@G12g\t\'w\t\t%??\t\"h$?F\"\"6%q\\\\{\tT\t\"]$87$}h\'\t<\t$\tc%U:mT2:<v\t#Rl!;U\t\t\"^D\tRZ(BZ{n\t%[$fgH`\t{B}:*\t\t%%*F`%W\t//B}PQ\t\tsu2\tLz<1*!p-X\tnKv&&0\thm4<n\\.\\/.w\'\t<)E1g3#ood\t`?\t\\({N?,8_?h\ty\t0%\t*$A\t\t*w-ViQUj\tTiH\t%\t%&0p\'\'\tA%r**Fo\'Z\\\tNI*ely4=I?}$.o`\t$\ts\'&lt\t\",:~=Nv/0O%=+<LS\t%P\'\t$r%d.\t{G{/L:\t&I&8-`cy*\"{\t/%fP9.P<\t\t\'/`\t\t`\t\t`!t:\t::\t\tW\'X^\t@.uL&a\tN\t\t\t.\t?0*\tvUK>UJ\\\tQbj%w]P=Js\t\"R\t&s^?</=%\t\'VI:\" kT`{b*<\t\tF&?Y\t\t:/\t!C\'e0`\t\t\tx-\t*\\N\\wwu.I4\tb/y\t\"P:P%\"\t\tQ][\\\t^x\t\t):\t\t&s\t$1-\t\t\tXt`0\t;\t/UeCP*\"G\t\t\':\tk@91Hf&#\t(Uzz&`p2;\t{]\t\"I_%=\\%,XD\"\'06QfCd<;$:q^\t8:\"*\"H\t\to\t&xK/\t\ty0/<}<j<|*`~::b\t=S.=}=Ki\t<Y.\'{\tf\t{Ub*H%(_4*?*\tn2\t/\'\t\t\t/,4]\tt\t<y\t\t\tWi\t\tT&\"\t\t\t\t\t=/1Wu?\t\'A\"W-P\t$?e\\\t`\t6`vD\t8`\t\tccD J\tY&jI//?_\t\\j\t_\tsiq$\t?9\tQ\t.^%&..?%Jm??A%+<\tN&*\t.g\tS$W\"\"\tMpq\t\t:&\\\thTz&<iz%/%T&\'F\t\\]&\t\t}\t\t\tXr=dtp`:+<,\t%60Y*<(&K*kJ\todO\t=<V?&\tMU/\"\t= Y!)<\tV\t9\t)\t&v8q{\t\t&pT\t3\ttB,qcq\'i$;gv%j_%M_{[\"&;\t\t\t.B;6Y\\%\t\"\tY/a\t\\`:?\t<\t?]\taNwD;\\\t%l*74%5T?QS :~yR/E*R\t\t=u\t\\\t\t.Q<;\\\t_S/{&F$^\tw_>\'h=|%\t\t:W!\\<U2\'$\tb&`\t=|=L9\t\t\t\\WZ:! }{\t ;\t;\t\t 0.*\t.%\"\'r%{<Mh_t[[}\t-\tJo\"b/AC*-=`=T\tz$2\tC\t\t/k{47\"\t\t,q%\tZ\tT3\t\tf>%\t\'?%@^tx\t7\"1Bk{b{n\t\"Pj3\tHc\t\tt\tY<\t#?\tSh\\yk/N\\\t8 7=R4*9Cw&m\t\\-\'f\t|\'#t(Etu.Hdu(E\t%&v:\'aqW~A5\t\t w.s{J%lX<\"\t\'*%m<&:/B<&\':U}$&`.{)\t\t6S\t:/$*kQ-Z\t^\'t${/tH*\'v\t3\t=\t\tDyp:B\t`I_R&4SO\t\t&-j=*.\t87&\'e*( \t\t\t\'<$\\DJ<$p?{}\'&\tv\t\\Xk<Y:Y`!$K{\tF&\tzd\t\t*i$\tj\'\t<)R*\t%?\t!.\t=\"@#~:=*\t\tXO=_T,1\"\'.%%\"`{\\:\t\"\tfkeOb/\'$I~\ta\t|&\t[\\KK\"1&Z\t<k\t\t)%\'-~\"2n\tj\tW?*<@w{g%d\ta\\\'\'I\t;:ySR%ke:4\tc\t$=\t&9P]x4\tJ=\t6C6%a\t`0\tF\tm-\tTr\t}\t\tQum\t&@\typ|w2&\t\t3`i&t\t\tT5\"\t.&b&e*/==1.\'*\\[U*\tqPL%?$-0/}~|q`\t\t}\t$\tq==o+T$\'!H\t\ti&um\"?\"%%\t/\'p\tg>?{0{J{\t\t/\t\t{zKZ&>=\t[\"1h<H%z/8,/]s\tv{7\t\t:j*H,M//\t\t\td\'.)\t"

View File

@@ -8,8 +8,8 @@ high level API reference
- :class:`deltachat.account.Account` (your main entry point, creates the
other classes)
- :class:`deltachat.contact.Contact`
- :class:`deltachat.chat.Chat`
- :class:`deltachat.chatting.Contact`
- :class:`deltachat.chatting.Chat`
- :class:`deltachat.message.Message`
Account
@@ -22,13 +22,13 @@ Account
Contact
-------
.. autoclass:: deltachat.contact.Contact
.. autoclass:: deltachat.chatting.Contact
:members:
Chat
----
.. autoclass:: deltachat.chat.Chat
.. autoclass:: deltachat.chatting.Chat
:members:
Message

View File

@@ -15,14 +15,12 @@ import deltachat
from . import const
from .capi import ffi, lib
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
from .chat import Chat
from .message import Message
from .contact import Contact
from .chatting import Contact, Chat, Message
class Account(object):
""" Each account is tied to a sqlite database file which is fully managed
by the underlying deltachat core library. All public Account methods are
by the underlying deltachat c-library. All public Account methods are
meant to be memory-safe and return memory-safe objects.
"""
def __init__(self, db_path, logid=None, eventlogging=True, debug=True):
@@ -162,9 +160,9 @@ class Account(object):
return from_dc_charpointer(lib.dc_get_blobdir(self._dc_context))
def get_self_contact(self):
""" return this account's identity as a :class:`deltachat.contact.Contact`.
""" return this account's identity as a :class:`deltachat.chatting.Contact`.
:returns: :class:`deltachat.contact.Contact`
:returns: :class:`deltachat.chatting.Contact`
"""
self.check_is_configured()
return Contact(self._dc_context, const.DC_CONTACT_ID_SELF)
@@ -176,7 +174,7 @@ class Account(object):
:param email: email-address (text type)
:param name: display name for this contact (optional)
:returns: :class:`deltachat.contact.Contact` instance.
:returns: :class:`deltachat.chatting.Contact` instance.
"""
name = as_dc_charpointer(name)
email = as_dc_charpointer(email)
@@ -202,7 +200,7 @@ class Account(object):
whose name or e-mail matches query.
:param only_verified: if true only return verified contacts.
:param with_self: if true the self-contact is also returned.
:returns: list of :class:`deltachat.contact.Contact` objects.
:returns: list of :class:`deltachat.chatting.Contact` objects.
"""
flags = 0
query = as_dc_charpointer(query)
@@ -220,7 +218,7 @@ class Account(object):
""" create or get an existing 1:1 chat object for the specified contact or contact id.
:param contact: chat_id (int) or contact object.
:returns: a :class:`deltachat.chat.Chat` object.
:returns: a :class:`deltachat.chatting.Chat` object.
"""
if hasattr(contact, "id"):
if contact._dc_context != self._dc_context:
@@ -237,7 +235,7 @@ class Account(object):
the specified message.
:param message: messsage id or message instance.
:returns: a :class:`deltachat.chat.Chat` object.
:returns: a :class:`deltachat.chatting.Chat` object.
"""
if hasattr(message, "id"):
if self._dc_context != message._dc_context:
@@ -255,7 +253,7 @@ class Account(object):
Chats are unpromoted until the first message is sent.
:param verified: if true only verified contacts can be added.
:returns: a :class:`deltachat.chat.Chat` object.
:returns: a :class:`deltachat.chatting.Chat` object.
"""
bytes_name = name.encode("utf8")
chat_id = lib.dc_create_group_chat(self._dc_context, int(verified), bytes_name)
@@ -264,7 +262,7 @@ class Account(object):
def get_chats(self):
""" return list of chats.
:returns: a list of :class:`deltachat.chat.Chat` objects.
:returns: a list of :class:`deltachat.chatting.Chat` objects.
"""
dc_chatlist = ffi.gc(
lib.dc_get_chatlist(self._dc_context, 0, ffi.NULL, 0),
@@ -282,24 +280,9 @@ class Account(object):
return Chat(self, const.DC_CHAT_ID_DEADDROP)
def get_message_by_id(self, msg_id):
""" return Message instance.
:param msg_id: integer id of this message.
:returns: :class:`deltachat.message.Message` instance.
"""
""" return Message instance. """
return Message.from_db(self, msg_id)
def get_chat_by_id(self, chat_id):
""" return Chat instance.
:param chat_id: integer id of this chat.
:returns: :class:`deltachat.chat.Chat` instance.
:raises: ValueError if chat does not exist.
"""
res = lib.dc_get_chat(self._dc_context, chat_id)
if res == ffi.NULL:
raise ValueError("cannot get chat with id={}".format(chat_id))
lib.dc_chat_unref(res)
return Chat(self, chat_id)
def mark_seen_messages(self, messages):
""" mark the given set of messages as seen.
@@ -316,7 +299,7 @@ class Account(object):
""" Forward list of messages to a chat.
:param messages: list of :class:`deltachat.message.Message` object.
:param chat: :class:`deltachat.chat.Chat` object.
:param chat: :class:`deltachat.chatting.Chat` object.
:returns: None
"""
msg_ids = [msg.id for msg in messages]
@@ -428,7 +411,7 @@ class Account(object):
""" setup contact and return a Chat after contact is established.
Note that this function may block for a long time as messages are exchanged
with the emitter of the QR code. On success a :class:`deltachat.chat.Chat` instance
with the emitter of the QR code. On success a :class:`deltachat.chatting.Chat` instance
is returned.
:param qr: valid "setup contact" QR code (all other QR codes will result in an exception)
"""
@@ -442,7 +425,7 @@ class Account(object):
""" join a chat group through a QR code.
Note that this function may block for a long time as messages are exchanged
with the emitter of the QR code. On success a :class:`deltachat.chat.Chat` instance
with the emitter of the QR code. On success a :class:`deltachat.chatting.Chat` instance
is returned which is the chat that we just joined.
:param qr: valid "join-group" QR code (all other QR codes will result in an exception)
@@ -509,20 +492,6 @@ class Account(object):
def on_dc_event_imex_file_written(self, data1, data2):
self._imex_events.put(data1)
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
"""set a new location. It effects all chats where we currently
have enabled location streaming.
:param latitude: float (use 0.0 if not known)
:param longitude: float (use 0.0 if not known)
:param accuracy: float (use 0.0 if not known)
:raises: ValueError if no chat is currently streaming locations
:returns: None
"""
dc_res = lib.dc_set_location(self._dc_context, latitude, longitude, accuracy)
if dc_res == 0:
raise ValueError("no chat is streaming locations")
class IOThreads:
def __init__(self, dc_context, log_event=lambda *args: None):

View File

@@ -1,15 +1,58 @@
""" Chat and Location related API. """
""" chatting related objects: Contact, Chat, Message. """
import mimetypes
import calendar
from datetime import datetime
import os
from . import props
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
from .capi import lib, ffi
from . import const
from .message import Message
class Contact(object):
""" Delta-Chat Contact.
You obtain instances of it through :class:`deltachat.account.Account`.
"""
def __init__(self, dc_context, id):
self._dc_context = dc_context
self.id = id
def __eq__(self, other):
return self._dc_context == other._dc_context and self.id == other.id
def __ne__(self, other):
return not (self == other)
def __repr__(self):
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self._dc_context)
@property
def _dc_contact(self):
return ffi.gc(
lib.dc_get_contact(self._dc_context, self.id),
lib.dc_contact_unref
)
@props.with_doc
def addr(self):
""" normalized e-mail address for this account. """
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
@props.with_doc
def display_name(self):
""" display name for this contact. """
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
def is_blocked(self):
""" Return True if the contact is blocked. """
return lib.dc_contact_is_blocked(self._dc_contact)
def is_verified(self):
""" Return True if the contact is verified. """
return lib.dc_contact_is_verified(self._dc_contact)
class Chat(object):
""" Chat object which manages members and through which you can send and retrieve messages.
@@ -269,10 +312,9 @@ class Chat(object):
def get_contacts(self):
""" get all contacts for this chat.
:params: contact object.
:returns: list of :class:`deltachat.contact.Contact` objects for this chat
:returns: list of :class:`deltachat.chatting.Contact` objects for this chat
"""
from .contact import Contact
dc_array = ffi.gc(
lib.dc_get_chat_contacts(self._dc_context, self.id),
lib.dc_array_unref
@@ -323,62 +365,3 @@ class Chat(object):
if dc_res == ffi.NULL:
return None
return from_dc_charpointer(dc_res)
# ------ location streaming API ------------------------------
def is_sending_locations(self):
"""return True if this chat has location-sending enabled currently.
:returns: True if location sending is enabled.
"""
return lib.dc_is_sending_locations_to_chat(self._dc_context, self.id)
def enable_sending_locations(self, seconds):
"""enable sending locations for this chat.
all subsequent messages will carry a location with them.
"""
lib.dc_send_locations_to_chat(self._dc_context, self.id, seconds)
def get_locations(self, contact=None, timestamp_from=None, timestamp_to=None):
"""return list of locations for the given contact in the given timespan.
:param contact: the contact for which locations shall be returned.
:param timespan_from: a datetime object or None (indicating "since beginning")
:param timespan_to: a datetime object or None (indicating up till now)
:returns: list of :class:`deltachat.chat.Location` objects.
"""
if timestamp_from is None:
time_from = 0
else:
time_from = calendar.timegm(timestamp_from.utctimetuple())
if timestamp_to is None:
time_to = 0
else:
time_to = calendar.timegm(timestamp_to.utctimetuple())
if contact is None:
contact_id = 0
else:
contact_id = contact.id
dc_array = lib.dc_get_locations(self._dc_context, self.id, contact_id, time_from, time_to)
return [
Location(
latitude=lib.dc_array_get_latitude(dc_array, i),
longitude=lib.dc_array_get_longitude(dc_array, i),
accuracy=lib.dc_array_get_accuracy(dc_array, i),
timestamp=datetime.utcfromtimestamp(lib.dc_array_get_timestamp(dc_array, i)))
for i in range(lib.dc_array_get_cnt(dc_array))
]
class Location:
def __init__(self, latitude, longitude, accuracy, timestamp):
assert isinstance(timestamp, datetime)
self.latitude = latitude
self.longitude = longitude
self.accuracy = accuracy
self.timestamp = timestamp
def __eq__(self, other):
return self.__dict__ == other.__dict__

View File

@@ -1,49 +0,0 @@
""" Contact object. """
from . import props
from .cutil import from_dc_charpointer
from .capi import lib, ffi
class Contact(object):
""" Delta-Chat Contact.
You obtain instances of it through :class:`deltachat.account.Account`.
"""
def __init__(self, dc_context, id):
self._dc_context = dc_context
self.id = id
def __eq__(self, other):
return self._dc_context == other._dc_context and self.id == other.id
def __ne__(self, other):
return not (self == other)
def __repr__(self):
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self._dc_context)
@property
def _dc_contact(self):
return ffi.gc(
lib.dc_get_contact(self._dc_context, self.id),
lib.dc_contact_unref
)
@props.with_doc
def addr(self):
""" normalized e-mail address for this account. """
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
@props.with_doc
def display_name(self):
""" display name for this contact. """
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
def is_blocked(self):
""" Return True if the contact is blocked. """
return lib.dc_contact_is_blocked(self._dc_contact)
def is_verified(self):
""" Return True if the contact is verified. """
return lib.dc_contact_is_verified(self._dc_contact)

View File

@@ -1,4 +1,4 @@
""" The Message object. """
""" chatting related objects: Contact, Chat, Message. """
import os
import shutil
@@ -13,7 +13,7 @@ class Message(object):
""" Message object.
You obtain instances of it through :class:`deltachat.account.Account` or
:class:`deltachat.chat.Chat`.
:class:`deltachat.chatting.Chat`.
"""
def __init__(self, account, dc_msg):
self.account = account
@@ -169,18 +169,18 @@ class Message(object):
def chat(self):
"""chat this message was posted in.
:returns: :class:`deltachat.chat.Chat` object
:returns: :class:`deltachat.chatting.Chat` object
"""
from .chat import Chat
from .chatting import Chat
chat_id = lib.dc_msg_get_chat_id(self._dc_msg)
return Chat(self.account, chat_id)
def get_sender_contact(self):
"""return the contact of who wrote the message.
:returns: :class:`deltachat.chat.Contact` instance
:returns: :class:`deltachat.chatting.Contact` instance
"""
from .contact import Contact
from .chatting import Contact
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
return Contact(self._dc_context, contact_id)

View File

@@ -121,12 +121,6 @@ class TestOfflineChat:
str(chat1)
repr(chat1)
def test_chat_by_id(self, chat1):
chat2 = chat1.account.get_chat_by_id(chat1.id)
assert chat2 == chat1
with pytest.raises(ValueError):
chat1.account.get_chat_by_id(123123)
def test_chat_idempotent(self, chat1, ac1):
contact1 = chat1.get_contacts()[0]
chat2 = ac1.create_chat_by_contact(contact1.id)
@@ -816,54 +810,6 @@ class TestOnlineAccount:
assert chat1b.get_profile_image() is None
assert chat.get_profile_image() is None
def test_send_receive_locations(self, acfactory, lp):
now = datetime.utcnow()
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1: create chat with ac2")
chat1 = self.get_chat(ac1, ac2)
chat2 = self.get_chat(ac2, ac1)
assert not chat1.is_sending_locations()
with pytest.raises(ValueError):
ac1.set_location(latitude=0.0, longitude=10.0)
ac1._evlogger.consume_events()
ac2._evlogger.consume_events()
lp.sec("ac1: enable location sending in chat")
chat1.enable_sending_locations(seconds=100)
assert chat1.is_sending_locations()
ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
ac1.set_location(latitude=2.0, longitude=3.0, accuracy=0.5)
ac1._evlogger.get_matching("DC_EVENT_LOCATION_CHANGED")
chat1.send_text("hello")
ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
lp.sec("ac2: wait for incoming location message")
ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG") # "enabled-location streaming"
# currently core emits location changed before event_incoming message
ac2._evlogger.get_matching("DC_EVENT_LOCATION_CHANGED")
ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG") # text message with location
locations = chat2.get_locations()
assert len(locations) == 1
assert locations[0].latitude == 2.0
assert locations[0].longitude == 3.0
assert locations[0].accuracy == 0.5
assert locations[0].timestamp > now
contact = ac2.create_contact(ac1.get_config("addr"))
locations2 = chat2.get_locations(contact=contact)
assert len(locations2) == 1
assert locations2 == locations
contact = ac2.create_contact("nonexisting@example.org")
locations3 = chat2.get_locations(contact=contact)
assert not locations3
class TestOnlineConfigureFails:
def test_invalid_password(self, acfactory):

View File

@@ -25,8 +25,7 @@ impl<'a> BlobObject<'a> {
/// Creates a new file in the blob directory. The name will be
/// derived from the platform-agnostic basename of the suggested
/// name, followed by a random number and followed by a possible
/// extension. The `data` will be written into the file without
/// race-conditions.
/// extension. The `data` will be written into the file.
///
/// # Errors
///
@@ -43,33 +42,29 @@ impl<'a> BlobObject<'a> {
data: &[u8],
) -> std::result::Result<BlobObject<'a>, BlobError> {
let blobdir = context.get_blobdir();
let (stem, ext) = BlobObject::sanitise_name(suggested_name.as_ref());
let (name, mut file) = BlobObject::create_new_file(&blobdir, &stem, &ext)?;
file.write_all(data)
.map_err(|err| BlobError::new_write_failure(blobdir, &name, err))?;
let blob = BlobObject {
blobdir,
name: format!("$BLOBDIR/{}", name),
};
context.call_cb(Event::NewBlobFile(blob.as_name().to_string()));
Ok(blob)
}
// Creates a new file, returning a tuple of the name and the handle.
fn create_new_file(dir: &Path, stem: &str, ext: &str) -> Result<(String, fs::File), BlobError> {
let max_attempt = 15;
let (stem, ext) = BlobObject::sanitise_name(suggested_name.as_ref().to_string());
let mut name = format!("{}{}", stem, ext);
let max_attempt = 15;
for attempt in 0..max_attempt {
let path = dir.join(&name);
let path = blobdir.join(&name);
match fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(&path)
{
Ok(file) => return Ok((name, file)),
Ok(mut file) => {
file.write_all(data)
.map_err(|err| BlobError::new_write_failure(blobdir, &name, err))?;
let blob = BlobObject {
blobdir,
name: format!("$BLOBDIR/{}", name),
};
context.call_cb(Event::NewBlobFile(blob.as_name().to_string()));
return Ok(blob);
}
Err(err) => {
if attempt == max_attempt {
return Err(BlobError::new_create_failure(dir, &name, err));
return Err(BlobError::new_create_failure(blobdir, &name, err));
} else {
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
}
@@ -77,7 +72,7 @@ impl<'a> BlobObject<'a> {
}
}
Err(BlobError::new_create_failure(
dir,
blobdir,
&name,
format_err!("Unreachable code - supposedly"),
))
@@ -86,9 +81,7 @@ impl<'a> BlobObject<'a> {
/// Creates a new blob object with unique name by copying an existing file.
///
/// This creates a new blob as described in [BlobObject::create]
/// but also copies an existing file into it. This is done in a
/// in way which avoids race-conditions when multiple files are
/// concurrently created.
/// but also copies an existing file into it.
///
/// # Errors
///
@@ -99,24 +92,11 @@ impl<'a> BlobObject<'a> {
context: &'a Context,
src: impl AsRef<Path>,
) -> std::result::Result<BlobObject<'a>, BlobError> {
let mut src_file = fs::File::open(src.as_ref()).map_err(|err| {
BlobError::new_copy_failure(context.get_blobdir(), "", src.as_ref(), err)
let blob = BlobObject::create(context, src.as_ref().to_string_lossy(), b"")?;
fs::copy(src.as_ref(), blob.to_abs_path()).map_err(|err| {
fs::remove_file(blob.to_abs_path()).ok();
BlobError::new_copy_failure(blob.blobdir, &blob.name, src.as_ref(), err)
})?;
let (stem, ext) = BlobObject::sanitise_name(&src.as_ref().to_string_lossy());
let (name, mut dst_file) = BlobObject::create_new_file(context.get_blobdir(), &stem, &ext)?;
std::io::copy(&mut src_file, &mut dst_file).map_err(|err| {
{
// Attempt to remove the failed file, swallow errors resulting from that.
let path = context.get_blobdir().join(&name);
fs::remove_file(path).ok();
}
BlobError::new_copy_failure(context.get_blobdir(), &name, src.as_ref(), err)
})?;
let blob = BlobObject {
blobdir: context.get_blobdir(),
name: format!("$BLOBDIR/{}", name),
};
context.call_cb(Event::NewBlobFile(blob.as_name().to_string()));
Ok(blob)
}
@@ -259,8 +239,7 @@ impl<'a> BlobObject<'a> {
/// ".txt")` while "bar" is returned as `("bar", "")`.
///
/// The extension part will always be lowercased.
fn sanitise_name(name: &str) -> (String, String) {
let mut name = name.to_string();
fn sanitise_name(mut name: String) -> (String, String) {
for part in name.rsplit('/') {
if part.len() > 0 {
name = part.to_string();

View File

@@ -258,7 +258,7 @@ impl Chat {
}
if (self.typ == Chattype::Group || self.typ == Chattype::VerifiedGroup)
&& !is_contact_in_chat(context, self.id, DC_CONTACT_ID_SELF)
&& !is_contact_in_chat(context, self.id, 1 as u32)
{
emit_event!(
context,
@@ -414,7 +414,7 @@ impl Chat {
&context.sql,
"INSERT INTO locations \
(timestamp,from_id,chat_id, latitude,longitude,independent)\
VALUES (?,?,?, ?,?,1);", // 1=DC_CONTACT_ID_SELF
VALUES (?,?,?, ?,?,1);",
params![
timestamp,
DC_CONTACT_ID_SELF,
@@ -446,7 +446,7 @@ impl Chat {
params![
new_rfc724_mid,
self.id as i32,
DC_CONTACT_ID_SELF,
1i32,
to_id as i32,
timestamp,
msg.type_0,
@@ -914,7 +914,7 @@ fn do_set_draft(context: &Context, chat_id: u32, msg: &mut Message) -> Result<()
VALUES (?,?,?, ?,?,?,?,?);",
params![
chat_id as i32,
DC_CONTACT_ID_SELF,
1,
time(),
msg.type_0,
MessageState::OutDraft,
@@ -993,8 +993,8 @@ pub fn get_chat_msgs(
" ON m.chat_id=chats.id",
" LEFT JOIN contacts",
" ON m.from_id=contacts.id",
" WHERE m.from_id!=1", // 1=DC_CONTACT_ID_SELF
" AND m.from_id!=2", // 2=DC_CONTACT_ID_DEVICE
" WHERE m.from_id!=1",
" AND m.from_id!=2",
" AND m.hidden=0",
" AND chats.blocked=2",
" AND contacts.blocked=0",
@@ -1350,7 +1350,7 @@ pub fn create_group_chat(
let chat_id = sql::get_rowid(context, &context.sql, "chats", "grpid", grpid);
if chat_id != 0 {
if add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF) {
if add_to_chat_contacts_table(context, chat_id, 1) {
let mut draft_msg = Message::new(Viewtype::Text);
draft_msg.set_text(Some(draft_txt));
set_draft_raw(context, chat_id, &mut draft_msg);
@@ -1557,7 +1557,7 @@ pub fn remove_contact_from_chat(
/* this allows to delete pending references to deleted contacts. Of course, this should _not_ happen. */
if let Ok(chat) = Chat::load_from_db(context, chat_id) {
if real_group_exists(context, chat_id) {
if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF) {
if !is_contact_in_chat(context, chat_id, 1 as u32) {
emit_event!(
context,
Event::ErrorSelfNotInGroup(
@@ -1653,7 +1653,7 @@ pub fn set_chat_name(
if real_group_exists(context, chat_id) {
if chat.name == new_name.as_ref() {
success = true;
} else if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF) {
} else if !is_contact_in_chat(context, chat_id, 1) {
emit_event!(
context,
Event::ErrorSelfNotInGroup("Cannot set chat name; self not in group".into())
@@ -1905,8 +1905,8 @@ pub fn add_device_msg(context: &Context, chat_id: u32, text: impl AsRef<str>) {
"INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,rfc724_mid) VALUES (?,?,?, ?,?,?, ?,?);",
params![
chat_id as i32,
DC_CONTACT_ID_DEVICE,
DC_CONTACT_ID_DEVICE,
2,
2,
dc_create_smeared_timestamp(context),
Viewtype::Text,
MessageState::InNoticed,

View File

@@ -295,7 +295,7 @@ impl Chatlist {
let mut lastcontact = None;
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id) {
if lastmsg.from_id != DC_CONTACT_ID_SELF
if lastmsg.from_id != 1
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
{
lastcontact = Contact::load_from_db(context, lastmsg.from_id).ok();

View File

@@ -531,26 +531,21 @@ fn try_smtp_one_param(context: &Context, param: &LoginParam) -> Option<bool> {
param.send_user, param.send_server, param.send_port, param.server_flags
);
info!(context, "Trying: {}", inf);
match context
if context
.smtp
.clone()
.lock()
.unwrap()
.connect(context, &param)
{
Ok(()) => {
info!(context, "success: {}", inf);
Some(true)
}
Err(err) => {
if context.shall_stop_ongoing() {
Some(false)
} else {
warn!(context, "could not connect: {}", err);
None
}
}
info!(context, "success: {}", inf);
return Some(true);
}
if context.shall_stop_ongoing() {
return Some(false);
}
info!(context, "could not connect: {}", inf);
None
}
/*******************************************************************************

View File

@@ -249,8 +249,6 @@ impl Default for Viewtype {
}
}
pub const DC_MSG_TRUNCATE_THRESHOLD:usize = 30000;
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -264,7 +264,7 @@ impl Contact {
.unwrap_or_default();
if addr_normalized == addr_self {
return DC_CONTACT_ID_SELF;
return 1;
}
context.sql.query_get_value(
@@ -301,7 +301,7 @@ impl Contact {
.unwrap_or_default();
if addr == addr_self {
return Ok((DC_CONTACT_ID_SELF, sth_modified));
return Ok((1, sth_modified));
}
if !may_be_valid_addr(&addr) {

View File

@@ -611,7 +611,7 @@ impl<'a> MimeParser<'a> {
let mut decoded_data = match wrapmime::mailmime_transfer_decode(mime) {
Ok(decoded_data) => decoded_data,
Err(_) => {
// Note that it's not always an error - might be no data
// Note that it's now always an error - might be no data
return false;
}
};
@@ -749,13 +749,27 @@ impl<'a> MimeParser<'a> {
return false;
}
}
self.do_add_single_file_part(
msg_type,
mime_type,
raw_mime.as_ref(),
&decoded_data,
&desired_filename,
);
if desired_filename.starts_with("location") && desired_filename.ends_with(".kml") {
if !decoded_data.is_empty() {
let d = std::string::String::from_utf8_lossy(&decoded_data);
self.location_kml = location::Kml::parse(self.context, &d).ok();
}
} else if desired_filename.starts_with("message")
&& desired_filename.ends_with(".kml")
{
if !decoded_data.is_empty() {
let d = std::string::String::from_utf8_lossy(&decoded_data);
self.message_kml = location::Kml::parse(self.context, &d).ok();
}
} else if !decoded_data.is_empty() {
self.do_add_single_file_part(
msg_type,
mime_type,
raw_mime.as_ref(),
&decoded_data,
&desired_filename,
);
}
}
_ => {}
}
@@ -769,44 +783,20 @@ impl<'a> MimeParser<'a> {
mime_type: libc::c_int,
raw_mime: Option<&String>,
decoded_data: &[u8],
filename: &str,
desired_filename: &str,
) {
if decoded_data.is_empty() {
return;
}
// treat location/message kml file attachments specially
if filename.ends_with(".kml") {
// XXX what if somebody sends eg an "location-highlights.kml"
// attachment unrelated to location streaming?
if filename.starts_with("location") || filename.starts_with("message") {
let parsed = location::Kml::parse(self.context, decoded_data)
.map_err(|err| {
warn!(self.context, "failed to parse kml part: {}", err);
})
.ok();
if filename.starts_with("location") {
self.location_kml = parsed;
} else {
self.message_kml = parsed;
}
return;
}
}
/* we have a regular file attachment,
write decoded data to new blob object */
let blob = match BlobObject::create(self.context, filename, decoded_data) {
/* write decoded data to new blob file */
let blob = match BlobObject::create(self.context, desired_filename, decoded_data) {
Ok(blob) => blob,
Err(err) => {
error!(
self.context,
"Could not add blob for mime part {}, error {}", filename, err
"Could not add blob for mime part {}, error {}", desired_filename, err
);
return;
}
};
/* create and register Mime part referencing the new Blob object */
let mut part = Part::default();
part.typ = msg_type;
part.mimetype = mime_type;
@@ -826,15 +816,10 @@ impl<'a> MimeParser<'a> {
}
fn do_add_single_part(&mut self, mut part: Part) {
if self.encrypted {
if self.signatures.len() > 0 {
part.param.set_int(Param::GuaranteeE2ee, 1);
} else {
// XXX if the message was encrypted but not signed
// it's not neccessarily an error we need to signal.
// we could just treat it as if it was not encrypted.
part.param.set_int(Param::ErroneousE2ee, 0x2);
}
if self.encrypted && self.signatures.len() > 0 {
part.param.set_int(Param::GuaranteeE2ee, 1);
} else if self.encrypted {
part.param.set_int(Param::ErroneousE2ee, 0x2);
}
self.parts.push(part);
}
@@ -1257,13 +1242,13 @@ mod tests {
#[ignore]
#[test]
fn test_dc_mailmime_parse_crash_fuzzy(data in "[!-~\t ]{2000,}") {
// this test doesn't exercise much of dc_mimeparser anymore
// because a missing From-field early aborts parsing
let context = dummy_context();
let mut mimeparser = MimeParser::new(&context.ctx);
// parsing should always succeed
// but the returned header will normally be empty on random data
assert!(unsafe {mimeparser.parse(data.as_bytes()).is_ok()});
assert!(mimeparser.header.is_empty());
unsafe {
assert!(mimeparser.parse(data.as_bytes()).is_err());
}
}
}

View File

@@ -227,7 +227,7 @@ pub unsafe fn dc_receive_imf(
&mut created_db_entries,
&mut create_event_to_send,
) {
warn!(context, "{}", err);
info!(context, "{}", err);
cleanup(
context,
@@ -257,7 +257,7 @@ pub unsafe fn dc_receive_imf(
);
}
if mime_parser.location_kml.is_some() || mime_parser.message_kml.is_some() {
if !mime_parser.message_kml.is_none() && chat_id > DC_CHAT_ID_LAST_SPECIAL {
save_locations(
context,
&mime_parser,
@@ -391,7 +391,7 @@ unsafe fn add_parts(
} else {
MessageState::InFresh
};
*to_id = DC_CONTACT_ID_SELF;
*to_id = 1;
// handshake messages must be processed _before_ chats are created
// (eg. contacs may be marked as verified)
if mime_parser.lookup_field("Secure-Join").is_some() {
@@ -551,9 +551,8 @@ unsafe fn add_parts(
if to_ids.is_empty() && 0 != to_self {
// from_id==to_id==DC_CONTACT_ID_SELF - this is a self-sent messages,
// maybe an Autocrypt Setup Messag
let (id, bl) =
chat::create_or_lookup_by_contact_id(context, DC_CONTACT_ID_SELF, Blocked::Not)
.unwrap_or_default();
let (id, bl) = chat::create_or_lookup_by_contact_id(context, 1, Blocked::Not)
.unwrap_or_default();
*chat_id = id;
chat_id_blocked = bl;
@@ -605,7 +604,6 @@ unsafe fn add_parts(
// into only one message; mails sent by other clients may result in several messages
// (eg. one per attachment))
let icnt = mime_parser.parts.len();
let mut txt_raw = None;
context.sql.prepare(
@@ -646,6 +644,17 @@ unsafe fn add_parts(
.set_int(Param::Cmd, mime_parser.is_system_message as i32);
}
/*
info!(
context,
"received mime message {:?}",
String::from_utf8_lossy(std::slice::from_raw_parts(
imf_raw_not_terminated as *const u8,
imf_raw_bytes,
))
);
*/
stmt.execute(params![
rfc724_mid,
server_folder.as_ref(),
@@ -841,13 +850,10 @@ fn save_locations(
insert_msg_id: MsgId,
hidden: i32,
) {
if chat_id <= DC_CHAT_ID_LAST_SPECIAL {
return ();
}
let mut location_id_written = false;
let mut send_event = false;
if mime_parser.message_kml.is_some() {
if !mime_parser.message_kml.is_none() && chat_id > DC_CHAT_ID_LAST_SPECIAL as libc::c_uint {
let locations = &mime_parser.message_kml.as_ref().unwrap().locations;
let newest_location_id =
location::save(context, chat_id, from_id, locations, 1).unwrap_or_default();
@@ -859,10 +865,12 @@ fn save_locations(
}
}
if mime_parser.location_kml.is_some() {
if !mime_parser.location_kml.is_none() && chat_id > DC_CHAT_ID_LAST_SPECIAL as libc::c_uint {
if let Some(ref addr) = mime_parser.location_kml.as_ref().unwrap().addr {
if let Ok(contact) = Contact::get_by_id(context, from_id) {
if contact.get_addr().to_lowercase() == addr.to_lowercase() {
if !contact.get_addr().is_empty()
&& contact.get_addr().to_lowercase() == addr.to_lowercase()
{
let locations = &mime_parser.location_kml.as_ref().unwrap().locations;
let newest_location_id =
location::save(context, chat_id, from_id, locations, 0).unwrap_or_default();
@@ -1351,8 +1359,8 @@ unsafe fn create_or_lookup_adhoc_group(
if !member_ids.contains(&from_id) {
member_ids.push(from_id);
}
if !member_ids.contains(&DC_CONTACT_ID_SELF) {
member_ids.push(DC_CONTACT_ID_SELF);
if !member_ids.contains(&1) {
member_ids.push(1);
}
if member_ids.len() < 3 {
// too few contacts given
@@ -1472,7 +1480,7 @@ fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> String {
.sql
.query_map(
format!(
"SELECT addr FROM contacts WHERE id IN({}) AND id!=1", // 1=DC_CONTACT_ID_SELF
"SELECT addr FROM contacts WHERE id IN({}) AND id!=1",
member_ids_str
),
params![],
@@ -1526,7 +1534,7 @@ fn search_chat_ids_by_contact_ids(
WHERE cc.chat_id IN(SELECT chat_id FROM chats_contacts WHERE contact_id IN({})) \
AND c.type=120 \
AND cc.contact_id!=1 \
ORDER BY cc.chat_id, cc.contact_id;", // 1=DC_CONTACT_ID_SELF
ORDER BY cc.chat_id, cc.contact_id;",
contact_ids_str
),
params![],

View File

@@ -249,10 +249,12 @@ pub(crate) fn dc_create_incoming_rfc724_mid(
contact_id_from: u32,
contact_ids_to: &[u32],
) -> Option<String> {
/* create a deterministic rfc724_mid from input such that
repeatedly calling it with the same input results in the same Message-id */
if contact_ids_to.is_empty() {
return None;
}
/* find out the largest receiver ID (we could also take the smallest, but it should be unique) */
let largest_id_to = contact_ids_to.iter().max().copied().unwrap_or_default();
let result = format!(
"{}-{}-{}@stub",
message_timestamp, contact_id_from, largest_id_to
@@ -1230,8 +1232,6 @@ mod tests {
fn test_dc_create_incoming_rfc724_mid() {
let res = dc_create_incoming_rfc724_mid(123, 45, &vec![6, 7]);
assert_eq!(res, Some("123-45-7@stub".into()));
let res = dc_create_incoming_rfc724_mid(123, 45, &vec![]);
assert_eq!(res, Some("123-45-0@stub".into()));
}
#[test]

View File

@@ -135,7 +135,8 @@ impl Job {
if !context.smtp.lock().unwrap().is_connected() {
let loginparam = LoginParam::from_database(context, "configured_");
let connected = context.smtp.lock().unwrap().connect(context, &loginparam);
if connected.is_err() {
if !connected {
self.try_again_later(3, None);
return;
}
@@ -172,10 +173,10 @@ impl Job {
// its ok/error response processing. Note that if a message
// was sent we need to mark it in the database ASAP as we
// otherwise might send it twice.
let mut smtp = context.smtp.lock().unwrap();
match smtp.send(context, recipients_list, body, self.job_id) {
let mut sock = context.smtp.lock().unwrap();
match sock.send(context, recipients_list, body) {
Err(err) => {
smtp.disconnect();
sock.disconnect();
warn!(context, "smtp failed: {}", err);
self.try_again_later(-1, Some(err.to_string()));
}
@@ -402,7 +403,7 @@ pub fn perform_imap_fetch(context: &Context) {
info!(
context,
"INBOX-fetch done in {:.4} ms.",
start.elapsed().as_nanos() as f64 / 1_000_000.0,
start.elapsed().as_nanos() as f64 / 1000.0,
);
}
@@ -1022,9 +1023,8 @@ pub fn interrupt_smtp_idle(context: &Context) {
}
pub fn interrupt_imap_idle(context: &Context) {
info!(context, "Interrupting INBOX-IDLE...",);
info!(context, "Interrupting IMAP-IDLE...",);
*context.perform_inbox_jobs_needed.write().unwrap() = true;
context.inbox.read().unwrap().interrupt_idle();
}

View File

@@ -15,7 +15,7 @@ pub struct JobThread {
#[derive(Clone, Debug, Default)]
pub struct JobState {
idle: bool,
jobs_needed: bool,
jobs_needed: i32,
suspended: bool,
using_handle: bool,
}
@@ -58,7 +58,7 @@ impl JobThread {
pub fn interrupt_idle(&self, context: &Context) {
{
self.state.0.lock().unwrap().jobs_needed = true;
self.state.0.lock().unwrap().jobs_needed = 1;
}
info!(context, "Interrupting {}-IDLE...", self.name);
@@ -139,13 +139,13 @@ impl JobThread {
let &(ref lock, ref cvar) = &*self.state.clone();
let mut state = lock.lock().unwrap();
if state.jobs_needed {
if 0 != state.jobs_needed {
info!(
context,
"{}-IDLE will not be started as it was interrupted while not ideling.",
self.name,
);
state.jobs_needed = false;
state.jobs_needed = 0;
return;
}

View File

@@ -62,11 +62,14 @@ impl Kml {
Default::default()
}
pub fn parse(context: &Context, content: &[u8]) -> Result<Self, Error> {
ensure!(content.len() <= 1024 * 1024, "kml-file is too large");
pub fn parse(context: &Context, content: impl AsRef<str>) -> Result<Self, Error> {
ensure!(
content.as_ref().len() <= (1024 * 1024),
"A kml-files with {} bytes is larger than reasonably expected.",
content.as_ref().len()
);
let to_parse = String::from_utf8_lossy(content);
let mut reader = quick_xml::Reader::from_str(&to_parse);
let mut reader = quick_xml::Reader::from_str(content.as_ref());
reader.trim_text(true);
let mut kml = Kml::new();
@@ -275,7 +278,7 @@ pub fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> b
accuracy,
time(),
chat_id,
DC_CONTACT_ID_SELF,
1,
]
) {
warn!(context, "failed to store location {:?}", err);
@@ -366,6 +369,9 @@ pub fn delete_all(context: &Context) -> Result<(), Error> {
}
pub fn get_kml(context: &Context, chat_id: u32) -> Result<(String, u32), Error> {
let now = time();
let mut location_count = 0;
let mut ret = String::new();
let mut last_added_location_id = 0;
let self_addr = context
@@ -382,17 +388,14 @@ pub fn get_kml(context: &Context, chat_id: u32) -> Result<(String, u32), Error>
Ok((send_begin, send_until, last_sent))
})?;
let now = time();
let mut location_count = 0;
let mut ret = String::new();
if locations_send_begin != 0 && now <= locations_send_until {
if !(locations_send_begin == 0 || now > locations_send_until) {
ret += &format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"{}\">\n",
self_addr,
);
context.sql.query_map(
"SELECT id, latitude, longitude, accuracy, timestamp \
"SELECT id, latitude, longitude, accuracy, timestamp\
FROM locations WHERE from_id=? \
AND timestamp>=? \
AND (timestamp>=? OR timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \
@@ -413,7 +416,7 @@ pub fn get_kml(context: &Context, chat_id: u32) -> Result<(String, u32), Error>
for row in rows {
let (location_id, latitude, longitude, accuracy, timestamp) = row?;
ret += &format!(
"<Placemark><Timestamp><when>{}</when></Timestamp><Point><coordinates accuracy=\"{}\">{},{}</coordinates></Point></Placemark>\n",
"<Placemark><Timestamp><when>{}</when></Timestamp><Point><coordinates accuracy=\"{}\">{},{}</coordinates></Point></Placemark>\n\x00",
timestamp,
accuracy,
longitude,
@@ -425,10 +428,10 @@ pub fn get_kml(context: &Context, chat_id: u32) -> Result<(String, u32), Error>
Ok(())
}
)?;
ret += "</Document>\n</kml>";
}
ensure!(location_count > 0, "No locations processed");
ret += "</Document>\n</kml>";
Ok((ret, last_added_location_id))
}
@@ -669,9 +672,9 @@ mod tests {
let context = dummy_context();
let xml =
b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"user@example.org\">\n<Placemark><Timestamp><when>2019-03-06T21:09:57Z</when></Timestamp><Point><coordinates accuracy=\"32.000000\">9.423110,53.790302</coordinates></Point></Placemark>\n<PlaceMARK>\n<Timestamp><WHEN > \n\t2018-12-13T22:11:12Z\t</WHEN></Timestamp><Point><coordinates aCCuracy=\"2.500000\"> 19.423110 \t , \n 63.790302\n </coordinates></Point></PlaceMARK>\n</Document>\n</kml>";
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"user@example.org\">\n<Placemark><Timestamp><when>2019-03-06T21:09:57Z</when></Timestamp><Point><coordinates accuracy=\"32.000000\">9.423110,53.790302</coordinates></Point></Placemark>\n<PlaceMARK>\n<Timestamp><WHEN > \n\t2018-12-13T22:11:12Z\t</WHEN></Timestamp><Point><coordinates aCCuracy=\"2.500000\"> 19.423110 \t , \n 63.790302\n </coordinates></Point></PlaceMARK>\n</Document>\n</kml>";
let kml = Kml::parse(&context.ctx, xml).expect("parsing failed");
let kml = Kml::parse(&context.ctx, &xml).expect("parsing failed");
assert!(kml.addr.is_some());
assert_eq!(kml.addr.as_ref().unwrap(), "user@example.org",);

View File

@@ -381,23 +381,10 @@ impl Message {
self.timestamp_sort
}
pub fn is_truncated(&self) -> bool {
if let Some(text) = &self.text {
return text.chars().count() > DC_MSG_TRUNCATE_THRESHOLD
}
false
}
pub fn get_text(&self, full_message: bool) -> Option<String> {
if full_message {
self.text
pub fn get_text(&self) -> Option<String> {
self.text
.as_ref()
.map(|text| text.to_string())
} else {
self.text
.as_ref()
.map(|text| dc_truncate(text, DC_MSG_TRUNCATE_THRESHOLD, false).to_string())
}
.map(|text| dc_truncate(text, 30000, false).to_string())
}
pub fn get_filename(&self) -> Option<String> {
@@ -727,7 +714,7 @@ pub fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
ret += "\n";
}
if msg.from_id == DC_CONTACT_ID_DEVICE || msg.to_id == DC_CONTACT_ID_DEVICE {
if msg.from_id == 2 || msg.to_id == 2 {
// device-internal message, no further details needed
return ret;
}

View File

@@ -484,7 +484,6 @@ impl<'a> MimeFactory<'a> {
if !meta_part.is_null() {
mailmime_smart_add_part(message, meta_part);
}
if self.msg.param.exists(Param::SetLatitude) {
let param = &self.msg.param;
let kml_file = location::get_message_kml(
@@ -501,21 +500,18 @@ impl<'a> MimeFactory<'a> {
}
if location::is_sending_locations_to_chat(context, self.msg.chat_id) {
match location::get_kml(context, self.msg.chat_id) {
Ok((kml_content, last_added_location_id)) => {
wrapmime::add_filename_part(
message,
"location.kml",
"application/vnd.google-earth.kml+xml",
&kml_content,
)?;
if !self.msg.param.exists(Param::SetLatitude) {
// otherwise, the independent location is already filed
self.out_last_added_location_id = last_added_location_id;
}
}
Err(err) => {
warn!(context, "mimefactory: could not get location: {}", err);
if let Ok((kml_file, last_added_location_id)) =
location::get_kml(context, self.msg.chat_id)
{
wrapmime::add_filename_part(
message,
"location.kml",
"application/vnd.google-earth.kml+xml",
&kml_file,
)?;
if !self.msg.param.exists(Param::SetLatitude) {
// otherwise, the independent location is already filed
self.out_last_added_location_id = last_added_location_id;
}
}
}

View File

@@ -44,24 +44,27 @@ impl Smtp {
}
/// Connect using the provided login params
pub fn connect(&mut self, context: &Context, lp: &LoginParam) -> Result<(), Error> {
pub fn connect(&mut self, context: &Context, lp: &LoginParam) -> bool {
if self.is_connected() {
warn!(context, "SMTP already connected.");
return Ok(());
return true;
}
if lp.send_server.is_empty() || lp.send_port == 0 {
context.call_cb(Event::ErrorNetwork("SMTP bad parameters.".into()));
bail!("SMTP Bad parameters");
}
self.from = match EmailAddress::new(lp.addr.clone()) {
Ok(addr) => Some(addr),
Err(err) => {
bail!("invalid login address {}: {}", lp.addr, err);
}
self.from = if let Ok(addr) = EmailAddress::new(lp.addr.clone()) {
Some(addr)
} else {
None
};
if self.from.is_none() {
// TODO: print error
return false;
}
let domain = &lp.send_server;
let port = lp.send_port as u16;
@@ -73,12 +76,11 @@ impl Smtp {
let addr = &lp.addr;
let send_pw = &lp.send_pw;
let access_token = dc_get_oauth2_access_token(context, addr, send_pw, false);
ensure!(
access_token.is_some(),
"could not get oaut2_access token addr={}",
addr
);
if access_token.is_none() {
return false;
}
let user = &lp.send_user;
(
lettre::smtp::authentication::Credentials::new(
user.to_string(),
@@ -123,27 +125,27 @@ impl Smtp {
"SMTP-LOGIN as {} ok",
lp.send_user,
)));
return Ok(());
return true;
}
Err(err) => {
bail!("SMTP: failed to connect {:?}", err);
warn!(context, "SMTP: failed to connect {:?}", err);
}
}
}
Err(err) => {
bail!("SMTP: failed to setup connection {:?}", err);
warn!(context, "SMTP: failed to setup connection {:?}", err);
}
}
false
}
/// SMTP-Send a prepared mail to recipients.
/// on successful send out Ok() is returned.
/// returns boolean whether send was successful.
pub fn send<'a>(
&mut self,
context: &Context,
recipients: Vec<EmailAddress>,
message: Vec<u8>,
job_id: u32,
) -> Result<(), Error> {
let message_len = message.len();
@@ -154,15 +156,12 @@ impl Smtp {
.join(",");
if let Some(ref mut transport) = self.transport {
let envelope = match Envelope::new(self.from.clone(), recipients) {
Ok(env) => env,
Err(err) => {
bail!("{}", err);
}
};
let envelope = Envelope::new(self.from.clone(), recipients);
ensure!(envelope.is_ok(), "internal smtp-message construction fail");
let envelope = envelope.unwrap();
let mail = SendableEmail::new(
envelope,
format!("{}", job_id), // only used for internal logging
"mail-id".into(), // TODO: random id
message,
);