Skip to content
Snippets Groups Projects
Forked from gajim / gajim-plugins
1544 commits behind, 519 commits ahead of the upstream repository.
  • Nikolay Yakimov's avatar
    ab4eeae9
    [gotr] Fix AttibuteError bug in get_control · ab4eeae9
    Nikolay Yakimov authored
    gotr expects fjid to be str, but it can be nbxmpp.JID. Converting to str
    shouldn't hurt, although could as well call fjid.getStripped() instead
    of gajim.get_jid_without_resource.
    
    Traceback (most recent call last):
      File "/usr/lib64/python2.7/site-packages/gajim/common/ged.py", line 93, in raise_event
        if handler(*args, **kwargs):
      File "/home/livid/.local/share/gajim/plugins/gotr/otrmodule.py", line 564, in handle_incoming_msg
        appdata={'thread':event.session.thread_id if event.session else None})
      File "/home/livid/.local/share/gajim/plugins/gotr/potr/context.py", line 210, in receiveMessage
        self.crypto.handleAKE(message, appdata=appdata)
      File "/home/livid/.local/share/gajim/plugins/gotr/potr/crypt.py", line 282, in handleAKE
        outMsg = self.ake.handleRevealSig(inMsg)
      File "/home/livid/.local/share/gajim/plugins/gotr/potr/crypt.py", line 420, in handleRevealSig
        self.onSuccess(self)
      File "/home/livid/.local/share/gajim/plugins/gotr/potr/crypt.py", line 316, in goEncrypted
        self.ctx._wentEncrypted()
      File "/home/livid/.local/share/gajim/plugins/gotr/potr/context.py", line 313, in _wentEncrypted
        self.setState(STATE_ENCRYPTED)
      File "/home/livid/.local/share/gajim/plugins/gotr/otrmodule.py", line 175, in setState
        OtrPlugin.update_otr(self.peer, self.user.accountname)
      File "/home/livid/.local/share/gajim/plugins/gotr/otrmodule.py", line 521, in update_otr
        ctrl = cls.get_control(user, acc)
      File "/home/livid/.local/share/gajim/plugins/gotr/otrmodule.py", line 535, in get_control
        gajim.get_jid_without_resource(fjid), account)
      File "/usr/lib64/python2.7/site-packages/gajim/common/gajim.py", line 297, in get_jid_without_resource
        return jid.split('/')[0]
    AttributeError: JID instance has no attribute 'split'
    [gotr] Fix AttibuteError bug in get_control
    Nikolay Yakimov authored
    gotr expects fjid to be str, but it can be nbxmpp.JID. Converting to str
    shouldn't hurt, although could as well call fjid.getStripped() instead
    of gajim.get_jid_without_resource.
    
    Traceback (most recent call last):
      File "/usr/lib64/python2.7/site-packages/gajim/common/ged.py", line 93, in raise_event
        if handler(*args, **kwargs):
      File "/home/livid/.local/share/gajim/plugins/gotr/otrmodule.py", line 564, in handle_incoming_msg
        appdata={'thread':event.session.thread_id if event.session else None})
      File "/home/livid/.local/share/gajim/plugins/gotr/potr/context.py", line 210, in receiveMessage
        self.crypto.handleAKE(message, appdata=appdata)
      File "/home/livid/.local/share/gajim/plugins/gotr/potr/crypt.py", line 282, in handleAKE
        outMsg = self.ake.handleRevealSig(inMsg)
      File "/home/livid/.local/share/gajim/plugins/gotr/potr/crypt.py", line 420, in handleRevealSig
        self.onSuccess(self)
      File "/home/livid/.local/share/gajim/plugins/gotr/potr/crypt.py", line 316, in goEncrypted
        self.ctx._wentEncrypted()
      File "/home/livid/.local/share/gajim/plugins/gotr/potr/context.py", line 313, in _wentEncrypted
        self.setState(STATE_ENCRYPTED)
      File "/home/livid/.local/share/gajim/plugins/gotr/otrmodule.py", line 175, in setState
        OtrPlugin.update_otr(self.peer, self.user.accountname)
      File "/home/livid/.local/share/gajim/plugins/gotr/otrmodule.py", line 521, in update_otr
        ctrl = cls.get_control(user, acc)
      File "/home/livid/.local/share/gajim/plugins/gotr/otrmodule.py", line 535, in get_control
        gajim.get_jid_without_resource(fjid), account)
      File "/usr/lib64/python2.7/site-packages/gajim/common/gajim.py", line 297, in get_jid_without_resource
        return jid.split('/')[0]
    AttributeError: JID instance has no attribute 'split'
otrmodule.py 28.03 KiB
#!/usr/bin/env python
# -*- coding: utf-8 -*-
##    otrmodule.py
##
## Copyright 2008-2012 Kjell Braden <afflux@pentabarf.de>
##
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
## by the Free Software Foundation; version 3 only.
##
## Gajim is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with Gajim.  If not, see <http://www.gnu.org/licenses/>.
##


'''
Off-The-Record encryption plugin.

:author: Kjell self.Braden <kb.otr@pentabarf.de>
:since: 2008
:copyright: Copyright 2008-2012 Kjell Braden <afflux@pentabarf.de>
:license: GPL
'''

MINVERSION = (1,0,0,'beta5')
MINCRYPTOVERSION = (2,1,0,'final',0)
IGNORE = True
PASS = False

DEFAULTFLAGS = {
            'ALLOW_V1':False,
            'ALLOW_V2':True,
            'REQUIRE_ENCRYPTION':False,
            'SEND_TAG':True,
            'WHITESPACE_START_AKE':True,
            'ERROR_START_AKE':True,
        }

MMS = 1024
PROTOCOL = 'xmpp'

MINVERSION_OUTGOING_MSG_STAZA = "0.16.4"

enc_tip = 'A private chat session <i>is established</i> to this contact ' \
        'with this fingerprint'
unused_tip = 'A private chat session is established to this contact using ' \
        '<i>another</i> fingerprint'
ended_tip = 'The private chat session to this contact has <i>ended</i>'
inactive_tip = 'Communication to this contact is currently ' \
        '<i>unencrypted</i>'
msg_not_send = _('Your message was not send. Either end '
                 'your private conversation, or restart it')


import logging
import nbxmpp
import os
import pickle
import time
import sys
from pprint import pformat
from distutils.version import LooseVersion

from common import gajim
from common import ged
from common.connection_handlers_events import MessageOutgoingEvent
from plugins import GajimPlugin
from message_control import TYPE_CHAT, MessageControl
from plugins.helpers import log_calls, log
from plugins.plugin import GajimPluginException

import ui

sys.path.insert(0, os.path.dirname(ui.__file__))

from HTMLParser import HTMLParser
from htmlentitydefs import name2codepoint
name2codepoint['apos'] = 0x0027

HAS_CRYPTO = True
try:
    import Crypto
    if not hasattr(Crypto, 'version_info') \
    or Crypto.version_info < MINCRYPTOVERSION:
        raise ImportError('PyCrypto not found or too old')
except ImportError:
    HAS_CRYPTO = False

HAS_POTR = True
try:
    import potr
    import potr.crypt
    import potr.context
    if not hasattr(potr, 'VERSION') or potr.VERSION < MINVERSION:
        raise ImportError('old / unsupported python-otr version')

    potrrootlog = logging.getLogger('potr')
    potrrootlog.handlers = []
    potrrootlog.propagate = False
    gajimrootlog = logging.getLogger('gajim')
    for h in gajimrootlog.handlers:
        potrrootlog.addHandler(h)

    def get_jid_from_fjid(fjid):
        return gajim.get_room_and_nick_from_fjid(str(fjid))[0]

    class GajimContext(potr.context.Context):
        # self.peer is fjid
        # self.jid does not contain resource
        __slots__ = ['smpWindow', 'jid']

        def __init__(self, account, peer):
            super(GajimContext, self).__init__(account, peer)
            self.jid = get_jid_from_fjid(peer)
            self.trustName = self.jid
            self.smpWindow = ui.ContactOtrSmpWindow(self)

        def inject(self, msg, appdata=None):
            log.debug('inject(appdata=%s)', appdata)
            msg = unicode(msg)
            account = self.user.accountname

            stanza = nbxmpp.Message(to=self.peer, body=msg, typ='chat')
            if appdata is not None:
                thread_id = appdata.get('thread', None)
                if thread_id is not None:
                    stanza.setThread(thread_id)
            add_message_processing_hints(stanza)
            gajim.connections[account].connection.send(stanza, now=True)

        def setState(self, newstate):
            if self.state == potr.context.STATE_ENCRYPTED:
                # we were encrypted
                if newstate == potr.context.STATE_ENCRYPTED:
                    # and are still -> it's just a refresh
                    OtrPlugin.gajim_log(
                            _('Private conversation with %s refreshed.') % self.peer,
                            self.user.accountname, self.peer)
                elif newstate == potr.context.STATE_FINISHED:
                    # and aren't anymore -> other side disconnected
                    OtrPlugin.gajim_log(_('%s has ended his/her private '
                            'conversation with you. You should do the same.')
                            % self.peer, self.user.accountname, self.peer)
            else:
                if newstate == potr.context.STATE_ENCRYPTED:
                    # we are now encrypted
                    trust = self.getCurrentTrust()
                    if trust is None:
                        fpr = str(self.getCurrentKey())
                        OtrPlugin.gajim_log(_('New fingerprint for %(peer)s: %(fpr)s')
                                % {'peer': self.peer, 'fpr': fpr},
                                self.user.accountname, self.peer)
                        self.setCurrentTrust('')
                    trustStr = 'authenticated' if bool(trust) else '*unauthenticated*'
                    OtrPlugin.gajim_log(
                        _('%(trustStr)s secured OTR conversation with %(peer)s started')
                        % {'trustStr': trustStr, 'peer': self.peer},
                        self.user.accountname, self.peer)

            if self.state != potr.context.STATE_PLAINTEXT and \
                    newstate == potr.context.STATE_PLAINTEXT:
                # we are now plaintext
                OtrPlugin.gajim_log(
                        _('Private conversation with %s lost.') % self.peer,
                        self.user.accountname, self.peer)

            super(GajimContext, self).setState(newstate)
            OtrPlugin.update_otr(self.peer, self.user.accountname)
            self.user.plugin.update_context_list()

        def getPolicy(self, key):
            ret = self.user.plugin.get_flags(self.user.accountname, self.jid)[key]
            log.debug('getPolicy(key=%s) = %s', key, ret)
            return ret

    class GajimOtrAccount(potr.context.Account):
        contextclass = GajimContext
        def __init__(self, plugin, accountname):
            global PROTOCOL, MMS
            self.plugin = plugin
            self.accountname = accountname
            name = gajim.get_jid_from_account(accountname)
            super(GajimOtrAccount, self).__init__(name, PROTOCOL, MMS)
            self.keyFilePath = os.path.join(gajim.gajimpaths.data_root, accountname)

        def dropPrivkey(self):
            try:
                os.remove(self.keyFilePath + '.key3')
            except IOError, e:
                if e.errno != 2:
                    log.exception('IOError occurred when removing key file for %s',
                            self.name)
            self.privkey = None

        def loadPrivkey(self):
            try:
                with open(self.keyFilePath + '.key3', 'rb') as keyFile:
                    return potr.crypt.PK.parsePrivateKey(keyFile.read())[0]
            except IOError, e:
                if e.errno != 2:
                    log.exception('IOError occurred when loading key file for %s',
                            self.name)
            return None

        def savePrivkey(self):
            try:
                with open(self.keyFilePath + '.key3', 'wb') as keyFile:
                    keyFile.write(self.getPrivkey().serializePrivateKey())
            except IOError, e:
                log.exception('IOError occurred when loading key file for %s',
                        self.name)

        def loadTrusts(self, newCtxCb=None):
            ''' load the fingerprint trustdb '''
            # it has the same format as libotr, therefore the
            # redundant account / proto field
            try:
                with open(self.keyFilePath + '.fpr', 'r') as fprFile:
                    for line in fprFile:
                        ctx, acc, proto, fpr, trust = line[:-1].split('\t')

                        if acc != self.name or proto != PROTOCOL:
                            continue

                        jid = get_jid_from_fjid(ctx)
                        self.setTrust(jid, fpr, trust)
            except IOError, e:
                if e.errno != 2:
                    log.exception('IOError occurred when loading fpr file for %s',
                            self.name)

        def saveTrusts(self):
            try:
                with open(self.keyFilePath + '.fpr', 'w') as fprFile:
                    for uid, trusts in self.trusts.iteritems():
                        for fpr, trustVal in trusts.iteritems():
                            fprFile.write('\t'.join(
                                    (uid, self.name, PROTOCOL, fpr, trustVal)))
                            fprFile.write('\n')
            except IOError, e:
                log.exception('IOError occurred when loading fpr file for %s',
                        self.name)
except ImportError:
    HAS_POTR = False

def otr_dialog_destroy(widget, *args, **kwargs):
    widget.destroy()


class OtrPlugin(GajimPlugin):
    otr = None
    def init(self):

        self.us = {}


        if not HAS_POTR:
            self.activatable = False
            self.available_text = _('Can\'t find potr. Verify this ' \
                    'plugin\'s integrity.')
            return

        if not HAS_CRYPTO:
            self.activatable = False
            self.available_text = _('PyCrypto not installed or too old.')
            return


        self.config_dialog = ui.OtrPluginConfigDialog(self)
        self.events_handlers = {}
        self.events_handlers['message-received'] = (ged.PRECORE,
                self.handle_incoming_msg)
        self.events_handlers['before-change-show'] = (ged.PRECORE,
                self.handle_change_show)
        if LooseVersion(gajim.config.get('version')) < LooseVersion(MINVERSION_OUTGOING_MSG_STAZA):
            self.events_handlers['message-outgoing'] = (ged.OUT_PRECORE,
                    self.handle_outgoing_msg)
        else:
            self.events_handlers['stanza-message-outgoing'] = (ged.OUT_PRECORE,
                    self.handle_outgoing_msg_stanza)
            DEFAULTFLAGS['SEND_TAG'] = False

        self.gui_extension_points = {
                    'chat_control' : (self.cc_connect, self.cc_disconnect)
                }

        for acc in gajim.contacts.get_accounts():
            self.us[acc] = GajimOtrAccount(self, acc)
            self.us[acc].loadTrusts()

            acc = str(acc)
            if acc not in self.config or None not in self.config[acc]:
                self.config[acc] = {None:DEFAULTFLAGS.copy()}
        self.update_context_list()

    @log_calls('OtrPlugin')
    def activate(self):
        if not HAS_CRYPTO or not HAS_POTR or not hasattr(potr, 'VERSION') \
        or potr.VERSION < MINVERSION:
            raise GajimPluginException(self.available_text)

    def get_otr_status(self, account, contact):
        ctx = self.us[account].getContext(contact.get_full_jid())

        finished = ctx.state == potr.context.STATE_FINISHED
        encrypted = finished or ctx.state == potr.context.STATE_ENCRYPTED
        trusted = encrypted and bool(ctx.getCurrentTrust())
        return (encrypted, trusted, finished)

    def cc_connect(self, cc):
        def update_otr(print_status=False):
            enc_status, authenticated, finished = \
                    self.get_otr_status(cc.account, cc.contact)
            otr_status_text = ''

            if finished:
                otr_status_text = u'finished OTR connection'
            elif authenticated:
                otr_status_text = u'authenticated secure OTR connection'
            elif enc_status:
                otr_status_text = u'*unauthenticated* secure OTR connection'

            cc._show_lock_image(enc_status, u'OTR', enc_status, True,
                    authenticated)
            if print_status and otr_status_text:
                cc.print_conversation_line(u'[OTR] %s' % otr_status_text,
                        'status', '', None)
        cc.update_otr = update_otr
        cc.update_otr(True)

        # hijack authentication button with our submenu
        def authbutton_cb(widget):
            if not cc.gpg_is_active and not (cc.session and
            cc.session.enable_encryption):
                ui.get_otr_submenu(self, cc).get_submenu().popup(None,
                        None, None, 0, 0)
            else:
                cc._on_authentication_button_clicked(widget)
        self.overwrite_handler(cc, cc.authentication_button, authbutton_cb)

        # hijack context menu
        cc.orig_prepare_context_menu = cc.prepare_context_menu
        def inject_menu(hide_buttonbar_items=False):
            menu = cc.orig_prepare_context_menu(hide_buttonbar_items)
            menu.insert(ui.get_otr_submenu(self, cc), 8)
            return menu
        cc.prepare_context_menu = inject_menu

    def cc_disconnect(self, cc):
        try:
            self.overwrite_handler(cc, cc.authentication_button,
                    cc._on_authentication_button_clicked)
            cc.prepare_context_menu = cc.orig_prepare_context_menu
            del cc.update_otr
        except AttributeError:
            pass

    def menu_settings_cb(self, item, control):
        ctx = self.us[control.account].getContext(control.contact.get_full_jid())
        dlg = ui.ContactOtrWindow(self, ctx)
        dlg.run()
        dlg.destroy()

    def menu_start_cb(self, item, control):
        gajim.nec.push_outgoing_event(MessageOutgoingEvent(None,
                account=control.account, jid=control.contact.jid,
                message=u'?OTRv?', type_='chat',
                resource=control.contact.resource, is_loggable=False))

    def menu_end_cb(self, item, control):
        fjid = control.contact.get_full_jid()
        thread_id = control.session.thread_id if control.session else None

        self.us[control.account].getContext(fjid).disconnect(
                appdata={'thread':thread_id})

    def menu_smp_cb(self, item, control):
        ctx = self.us[control.account].getContext(control.contact.get_full_jid())
        ctx.smpWindow.show(False)

    @staticmethod
    def overwrite_handler(window, control, handler):
        for id_, v in window.handlers.iteritems():
            if v == control:
                break
        else:
            raise LookupError

        del window.handlers[id_]
        control.disconnect(id_)
        id_ = control.connect('clicked', handler)
        window.handlers[id_] = control

    def set_flags(self, value, account=None, contact=None):
        if isinstance(account, unicode):
            account = account.encode()

        if account not in self.config:
            self.config[account] = {None:DEFAULTFLAGS.copy()}

        if account is None and contact is not None:
            # don't set per-contact options without account
            raise Exception("can't set contact flags without account")

        config = self.config[account]
        config[contact] = value

        self.config[account] = config

    def get_flags(self, account=None, contact=None, fallback=True):
        if isinstance(account, unicode):
            account = account.encode()

        setting = DEFAULTFLAGS.copy()
        if account in self.config:
            setting.update(self.config[account][None])
            if contact in self.config[account] \
                    and self.config[account][contact] is not None:
                setting.update(self.config[account][contact])
            elif not fallback:
                return None
        return setting

    def update_context_list(self):
        self.config_dialog.fpr_model.clear()
        for us in self.us.itervalues():
            usedFpr = set()
            for fjid, ctx in us.ctxs.iteritems():
                # get active contexts first
                key = ctx.getCurrentKey()
                if not key:
                    continue
                fpr = key.cfingerprint()
                usedFpr.add(fpr)

                human_hash = potr.human_hash(fpr)
                trust = bool(us.getTrust(ctx.trustName, fpr))

                if ctx.state == potr.context.STATE_ENCRYPTED:
                    state = "encrypted"
                    tip = enc_tip
                elif ctx.state == potr.context.STATE_FINISHED:
                    state = "finished"
                    tip = ended_tip
                else:
                    state = 'inactive'
                    tip = inactive_tip

                self.config_dialog.fpr_model.append((fjid, state, trust,
                        '<tt>%s</tt>' % human_hash, us.name, tip, fpr))

            for uid, trusts in us.trusts.iteritems():
                for fpr, trust in trusts.iteritems():
                    if fpr in usedFpr:
                        continue

                    state = 'inactive'
                    tip = inactive_tip

                    human_hash = potr.human_hash(fpr)

                    self.config_dialog.fpr_model.append((uid, state, bool(trust),
                            '<tt>%s</tt>' % human_hash, us.name, tip, fpr))

    @classmethod
    def gajim_log(cls, msg, account, fjid, no_print=False,
    is_status_message=True, thread_id=None):
        if not isinstance(fjid, unicode):
            fjid = unicode(fjid)
        if not isinstance(account, unicode):
            account = unicode(account)

        resource = gajim.get_resource_from_jid(fjid)
        jid = gajim.get_jid_without_resource(fjid)
        tim = time.localtime()

        if is_status_message is True:
            if not no_print:
                ctrl = cls.get_control(fjid, account)
                if ctrl:
                    ctrl.print_conversation_line(u'[OTR] %s' % msg, 'status',
                            '', None)
            if gajim.config.should_log(account, jid):
                id = gajim.logger.write('chat_msg_recv', fjid,
                        message=u'[OTR: %s]' % msg, tim=tim)
                # gajim.logger.write() only marks a message as unread (and so
                # only returns an id) when fjid is a real contact (NOT if it's a
                # GC private chat)
                if id:
                    gajim.logger.set_read_messages([id])
        else:
            session = gajim.connections[account].get_or_create_session(fjid,
                    thread_id)
            session.received_thread_id |= bool(thread_id)
            session.last_receive = time.time()

            if not session.control:
                # look for an existing chat control without a session
                ctrl = cls.get_control(fjid, account)
                if ctrl:
                    session.control = ctrl
                    session.control.set_session(session)

            if gajim.config.should_log(account, jid):
                msg_id = gajim.logger.write('chat_msg_recv', fjid,
                        message=u'[OTR: %s]' % msg, tim=tim)
                session.roster_message(jid, msg, tim=tim, msg_id=msg_id,
                        msg_type='chat', resource=resource)

    @classmethod
    def update_otr(cls, user, acc, print_status=False):
        ctrl = cls.get_control(user, acc)
        if ctrl:
            ctrl.update_otr(print_status)

    @staticmethod
    def get_control(fjid, account):
        # first try to get the window with the full jid
        ctrl = gajim.interface.msg_win_mgr.get_control(fjid, account)
        if ctrl:
            # got one, be happy
            return ctrl

        # otherwise try without the resource
        ctrl = gajim.interface.msg_win_mgr.get_control(
                gajim.get_jid_without_resource(str(fjid)), account)
        # but only use it when it's not a GC window
        if ctrl and ctrl.TYPE_ID == TYPE_CHAT:
            return ctrl

    def handle_change_show(self, event):
        account = event.conn.name

        if event.show == 'offline':
            for us in self.us.itervalues():
                for fjid, ctx in us.ctxs.iteritems():
                    if ctx.state == potr.context.STATE_ENCRYPTED:
                        self.us[account].getContext(fjid).disconnect()

        return PASS

    def handle_incoming_msg(self, event):
        ctx = None
        account = event.conn.name
        accjid = gajim.get_jid_from_account(account)

        if event.encrypted is not False or not event.stanza.getTag('body') \
            or not isinstance(event.stanza.getBody(), unicode) \
                or event.mtype != 'chat':
            return PASS

        try:
            ctx = self.us[account].getContext(event.fjid)
            msgtxt, tlvs = ctx.receiveMessage(event.msgtxt.encode('utf8'),
                            appdata={'thread':event.session.thread_id if event.session else None})
        except potr.context.NotOTRMessage, e:
            # received message was not OTR - pass it on
            return PASS
        except potr.context.UnencryptedMessage, e:
            # we are encrypted but got some plaintext
            # display it with a warning
            tlvs = []
            msgtxt = _('The following message received from %(jid)s was '
                    '*not encrypted*: [%(error)s]') % {'jid': event.fjid,
                    'error': e.args[0]}
        except potr.context.NotEncryptedError, e:
            # we got some encrypted data
            # but we don't have an encrypted session
            self.gajim_log(_('The encrypted message received from %s is '
                    'unreadable, as you are not currently communicating '
                    'privately') % event.fjid, account, event.fjid)
            return IGNORE
        except potr.context.ErrorReceived, e:
            # got a protocol error
            self.gajim_log(_('We received the following OTR error '
                    'message from %(jid)s: [%(error)s]') % {'jid': event.fjid,
                    'error': e.args[0].error},
                    account, event.fjid)
            return IGNORE
        except potr.crypt.InvalidParameterError, e:
            # received a packet we cannot process (probably tampered or
            # sent to wrong session)
            self.gajim_log(_('We received an unreadable OTR message '
                    'from %(jid)s. It has probably been tampered with, '
                    'or was sent from an older OTR session.')
                    % {'jid':event.fjid}, account, event.fjid)
            return IGNORE
        except RuntimeError, e:
            # generic library bug?
            self.gajim_log(_('The following error occurred when trying to '
                    'decrypt a message from %(jid)s: [%(error)s]') % {
                    'jid': event.fjid, 'error': e},
                    account, event.fjid)
            return IGNORE

        if ctx is not None:
            ctx.smpWindow.handle_tlv(tlvs)

        stripper = HTMLStripper()
        stripper.feed((msgtxt or '').decode('utf8'))
        event.msgtxt = stripper.stripped_data
        event.stanza.setBody(event.msgtxt)
        if event.stanza.getXHTML():
            event.stanza.delChild('html')
        event.stanza.setXHTML((msgtxt or '').decode('utf8'))

        return PASS

    def handle_outgoing_msg_stanza(self, event):
        xhtml = event.msg_iq.getXHTML()
        body = event.msg_iq.getBody()
        encrypted = False
        thread_id = event.msg_iq.getThread()
        try:
            if xhtml:
                xhtml = xhtml.encode('utf8')
                encrypted_msg = self.us[event.conn.name].\
                    getContext(event.msg_iq.getTo()).\
                    sendMessage(potr.context.FRAGMENT_SEND_ALL_BUT_LAST, xhtml,
                        appdata={'thread': thread_id})
                if xhtml != encrypted_msg.strip(): #.strip() because sendMessage() adds whitespaces
                    encrypted = True
                    event.msg_iq.delChild('html')
                    event.msg_iq.setBody(encrypted_msg)
            elif body:
                body = escape(body).encode('utf8')
                encrypted_msg = self.us[event.conn.name].\
                    getContext(event.msg_iq.getTo()).\
                    sendMessage(potr.context.FRAGMENT_SEND_ALL_BUT_LAST, body,
                        appdata={'thread': thread_id})
                if body != encrypted_msg.strip():
                    encrypted = True
                    event.msg_iq.setBody(encrypted_msg)
        except potr.context.NotEncryptedError, e:
            if e.args[0] == potr.context.EXC_FINISHED:
                self.gajim_log(msg_not_send, event.conn.name, event.msg_iq.getTo())
                return IGNORE
            else:
                raise e

        if encrypted:
            add_message_processing_hints(event.msg_iq)

        return PASS

    def handle_outgoing_msg(self, event):
        try:
            if hasattr(event, 'otrmessage'):
                return PASS

            xep_200 = bool(event.session) and event.session.enable_encryption

            potrrootlog.debug('got event {0} xep_200={1}'.format(pformat(event.__dict__), xep_200))
            if xep_200 or not event.message:
                return PASS

            if event.session:
                fjid = event.session.get_to()
            else:
                fjid = event.jid
                if event.resource:
                    fjid += '/' + event.resource

            message = event.xhtml or escape(event.message)
            message = message.encode('utf8')

            potrrootlog.debug('processing message={0!r} from fjid={1!r}'.format(message, fjid))

            try:
                newmsg = self.us[event.account].getContext(fjid).sendMessage(
                        potr.context.FRAGMENT_SEND_ALL_BUT_LAST, message,
                        appdata={'thread':event.session.thread_id if event.session else None})
                potrrootlog.debug('processed message={0!r}'.format(newmsg))
            except potr.context.NotEncryptedError, e:
                if e.args[0] == potr.context.EXC_FINISHED:
                    self.gajim_log(msg_not_send, event.account, fjid)
                    return IGNORE
                else:
                    raise e

            if event.xhtml: # if we had html before, replace with new content
                event.xhtml = newmsg

            stripper = HTMLStripper()
            stripper.feed((newmsg or '').decode('utf8'))
            event.message = stripper.stripped_data

            return PASS

        except:
            potrrootlog.exception('exception in outgoing message handler, message (hopefully) discarded')
            return IGNORE

class HTMLStripper(HTMLParser):
    def reset(self):
        self.stripped_data = ''
        HTMLParser.reset(self)

    def handle_data(self, data):
        self.stripped_data += data

    def handle_starttag(self, tag, attrs):
        if tag == 'br':
            self.stripped_data += '\n'

    def handle_entityref(self, name):
        try:
            c = unichr(name2codepoint[name])
        except KeyError:
            c = '&{};'.format(name)
        self.stripped_data += c

    def handle_charref(self, name):
        if name.startswith('x'):
            c = unichr(int(name[1:], 16))
        else:
            c = unichr(int(name))
        self.stripped_data += c

    def unknown_decl(self, data):
        if data.startswith('CDATA['):
            self.stripped_data += data[6:]

    def feed(self, data):
        data = data.replace('\n', '')
        HTMLParser.feed(self, data)

def escape(s):
    '''Replace special characters "&", "<" and ">" to HTML-safe sequences.
    If the optional flag quote is true, the quotation mark character (")
    is also translated.'''
    s = s.replace("&", "&amp;") # Must be done first!
    s = s.replace("<", "&lt;")
    s = s.replace(">", "&gt;")
    s = s.replace("\n", "<br/>")
    return s

def add_message_processing_hints(stanza):
    stanza.addChild(name='private', namespace=nbxmpp.NS_CARBONS)
    stanza.addChild(name='no-permanent-store', namespace=nbxmpp.NS_MSG_HINTS)
    stanza.addChild(name='no-copy', namespace=nbxmpp.NS_MSG_HINTS)