Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
P
PIM Messagelib
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
1
Issues
1
List
Boards
Labels
Service Desk
Milestones
Merge Requests
4
Merge Requests
4
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Operations
Operations
Incidents
Environments
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
PIM
PIM Messagelib
Commits
9bb1dd91
Commit
9bb1dd91
authored
Mar 30, 2016
by
Sandro Knauß
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
move okDecryptMime to CryptoMessagePart
parent
46e73409
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
154 additions
and
208 deletions
+154
-208
mimetreeparser/src/viewer/messagepart.cpp
mimetreeparser/src/viewer/messagepart.cpp
+145
-17
mimetreeparser/src/viewer/messagepart.h
mimetreeparser/src/viewer/messagepart.h
+9
-0
mimetreeparser/src/viewer/objecttreeparser.cpp
mimetreeparser/src/viewer/objecttreeparser.cpp
+0
-179
mimetreeparser/src/viewer/objecttreeparser.h
mimetreeparser/src/viewer/objecttreeparser.h
+0
-12
No files found.
mimetreeparser/src/viewer/messagepart.cpp
View file @
9bb1dd91
...
...
@@ -27,6 +27,8 @@
#include "job/kleojobexecutor.h"
#include "utils/iconnamecache.h"
#include "memento/decryptverifybodypartmemento.h"
#include <MessageCore/StringUtil>
#include <libkleo/importjob.h>
...
...
@@ -46,6 +48,8 @@
#include <KLocalizedString>
#include <KEmailAddress>
#include <sstream>
using
namespace
MimeTreeParser
;
/** Checks whether @p str contains external references. To be precise,
...
...
@@ -1666,6 +1670,7 @@ CryptoMessagePart::CryptoMessagePart(ObjectTreeParser *otp,
KMime
::
Content
*
node
)
:
MessagePart
(
otp
,
text
)
,
mPassphraseError
(
false
)
,
mNoSecKey
(
false
)
,
mCryptoProto
(
cryptoProto
)
,
mFromAddress
(
fromAddress
)
,
mNode
(
node
)
...
...
@@ -1724,6 +1729,116 @@ void CryptoMessagePart::startDecryption(const QByteArray &text, const QTextCodec
}
}
bool
CryptoMessagePart
::
okDecryptMIME
(
KMime
::
Content
&
data
)
{
mPassphraseError
=
false
;
mMetaData
.
inProgress
=
false
;
mMetaData
.
errorText
.
clear
();
mMetaData
.
auditLogError
=
GpgME
::
Error
();
mMetaData
.
auditLog
.
clear
();
bool
bDecryptionOk
=
false
;
bool
cannotDecrypt
=
false
;
ObjectTreeSourceIf
*
source
=
mOtp
->
mSource
;
NodeHelper
*
nodeHelper
=
mOtp
->
nodeHelper
();
assert
(
decryptMessage
());
// Check whether the memento contains a result from last time:
const
DecryptVerifyBodyPartMemento
*
m
=
dynamic_cast
<
DecryptVerifyBodyPartMemento
*>
(
nodeHelper
->
bodyPartMemento
(
&
data
,
"decryptverify"
));
assert
(
!
m
||
mCryptoProto
);
//No CryptoPlugin and having a bodyPartMemento -> there is something completly wrong
if
(
!
m
&&
mCryptoProto
)
{
Kleo
::
DecryptVerifyJob
*
job
=
mCryptoProto
->
decryptVerifyJob
();
if
(
!
job
)
{
cannotDecrypt
=
true
;
}
else
{
const
QByteArray
ciphertext
=
data
.
decodedContent
();
DecryptVerifyBodyPartMemento
*
newM
=
new
DecryptVerifyBodyPartMemento
(
job
,
ciphertext
);
if
(
mOtp
->
allowAsync
())
{
QObject
::
connect
(
newM
,
&
CryptoBodyPartMemento
::
update
,
nodeHelper
,
&
NodeHelper
::
update
);
QObject
::
connect
(
newM
,
SIGNAL
(
update
(
MimeTreeParser
::
UpdateMode
)),
source
->
sourceObject
(),
SLOT
(
update
(
MimeTreeParser
::
UpdateMode
)));
if
(
newM
->
start
())
{
mMetaData
.
inProgress
=
true
;
mOtp
->
mHasPendingAsyncJobs
=
true
;
}
else
{
m
=
newM
;
}
}
else
{
newM
->
exec
();
m
=
newM
;
}
nodeHelper
->
setBodyPartMemento
(
&
data
,
"decryptverify"
,
newM
);
}
}
else
if
(
m
->
isRunning
())
{
mMetaData
.
inProgress
=
true
;
mOtp
->
mHasPendingAsyncJobs
=
true
;
m
=
0
;
}
if
(
m
)
{
const
QByteArray
&
plainText
=
m
->
plainText
();
const
GpgME
::
DecryptionResult
&
decryptResult
=
m
->
decryptResult
();
const
GpgME
::
VerificationResult
&
verifyResult
=
m
->
verifyResult
();
mMetaData
.
isSigned
=
verifyResult
.
signatures
().
size
()
>
0
;
mSignatures
=
verifyResult
.
signatures
();
mDecryptRecipients
=
decryptResult
.
recipients
();
bDecryptionOk
=
!
decryptResult
.
error
();
mMetaData
.
auditLogError
=
m
->
auditLogError
();
mMetaData
.
auditLog
=
m
->
auditLogAsHtml
();
// std::stringstream ss;
// ss << decryptResult << '\n' << verifyResult;
// qCDebug(MIMETREEPARSER_LOG) << ss.str().c_str();
if
(
!
bDecryptionOk
&&
mMetaData
.
isSigned
)
{
//Only a signed part
mMetaData
.
isEncrypted
=
false
;
bDecryptionOk
=
true
;
mDecryptedData
=
plainText
;
}
else
{
mPassphraseError
=
decryptResult
.
error
().
isCanceled
()
||
decryptResult
.
error
().
code
()
==
GPG_ERR_NO_SECKEY
;
mMetaData
.
isEncrypted
=
decryptResult
.
error
().
code
()
!=
GPG_ERR_NO_DATA
;
mMetaData
.
errorText
=
QString
::
fromLocal8Bit
(
decryptResult
.
error
().
asString
());
if
(
mMetaData
.
isEncrypted
&&
decryptResult
.
numRecipients
()
>
0
)
{
mMetaData
.
keyId
=
decryptResult
.
recipient
(
0
).
keyID
();
}
if
(
bDecryptionOk
)
{
mDecryptedData
=
plainText
;
}
else
{
mNoSecKey
=
true
;
foreach
(
const
GpgME
::
DecryptionResult
::
Recipient
&
recipient
,
decryptResult
.
recipients
())
{
mNoSecKey
&=
(
recipient
.
status
().
code
()
==
GPG_ERR_NO_SECKEY
);
}
}
}
}
if
(
!
bDecryptionOk
)
{
QString
cryptPlugLibName
;
if
(
mCryptoProto
)
{
cryptPlugLibName
=
mCryptoProto
->
name
();
}
if
(
!
mCryptoProto
)
{
mMetaData
.
errorText
=
i18n
(
"No appropriate crypto plug-in was found."
);
}
else
if
(
cannotDecrypt
)
{
mMetaData
.
errorText
=
i18n
(
"Crypto plug-in
\"
%1
\"
cannot decrypt messages."
,
cryptPlugLibName
);
}
else
if
(
!
passphraseError
())
{
mMetaData
.
errorText
=
i18n
(
"Crypto plug-in
\"
%1
\"
could not decrypt the data."
,
cryptPlugLibName
)
+
QLatin1String
(
"<br />"
)
+
i18n
(
"Error: %1"
,
mMetaData
.
errorText
);
}
}
return
bDecryptionOk
;
}
void
CryptoMessagePart
::
startDecryption
(
KMime
::
Content
*
data
)
{
if
(
!
mNode
&&
!
data
)
{
...
...
@@ -1734,27 +1849,15 @@ void CryptoMessagePart::startDecryption(KMime::Content *data)
data
=
mNode
;
}
bool
signatureFound
;
bool
actuallyEncrypted
=
true
;
bool
decryptionStarted
;
mMetaData
.
isEncrypted
=
true
;
CryptoProtocolSaver
saver
(
mOtp
,
mCryptoProto
);
bool
bOkDecrypt
=
mOtp
->
okDecryptMIME
(
*
data
,
mDecryptedData
,
signatureFound
,
mSignatures
,
true
,
mPassphraseError
,
actuallyEncrypted
,
decryptionStarted
,
mMetaData
);
if
(
decryptionStarted
)
{
mMetaData
.
inProgress
=
true
;
bool
bOkDecrypt
=
okDecryptMIME
(
*
data
);
if
(
mMetaData
.
inProgress
)
{
return
;
}
mMetaData
.
isDecryptable
=
bOkDecrypt
;
mMetaData
.
isEncrypted
=
actuallyEncrypted
;
mMetaData
.
isSigned
=
signatureFound
;
if
(
!
mMetaData
.
isDecryptable
)
{
setText
(
QString
::
fromUtf8
(
mDecryptedData
.
constData
()));
...
...
@@ -1872,7 +1975,32 @@ void CryptoMessagePart::html(bool decorate)
// In progress has no special body
}
else
if
(
mMetaData
.
isEncrypted
&&
!
mMetaData
.
isDecryptable
)
{
const
CryptoBlock
block
(
mOtp
,
&
mMetaData
,
mCryptoProto
,
mOtp
->
mSource
,
mFromAddress
,
mNode
);
writer
->
queue
(
text
());
//Do not quote ErrorText
const
QString
errorMsg
=
i18n
(
"Could not decrypt the data."
);
const
QString
sNoSecKeyHeader
=
i18n
(
"No secret key found to encrypt the message. It is encrypted for following keys:"
);
QString
secKeyList
;
if
(
mNoSecKey
)
{
foreach
(
const
GpgME
::
DecryptionResult
::
Recipient
&
recipient
,
mDecryptRecipients
)
{
if
(
!
secKeyList
.
isEmpty
())
{
secKeyList
+=
QStringLiteral
(
"<br />"
);
}
secKeyList
+=
QStringLiteral
(
"<a href=
\"
kmail:showCertificate#%1 ### %2 ### %3
\"
>%4</a>"
)
.
arg
(
mCryptoProto
->
displayName
(),
mCryptoProto
->
name
(),
QString
::
fromLatin1
(
recipient
.
keyID
()),
QString
::
fromLatin1
(
QByteArray
(
"0x"
)
+
recipient
.
keyID
())
);
}
}
writer
->
queue
(
QStringLiteral
(
"<div style=
\"
font-size:x-large; text-align:center; padding:20pt;
\"
>"
));
if
(
mNoSecKey
)
{
writer
->
queue
(
sNoSecKeyHeader
+
QStringLiteral
(
"<br />"
)
+
secKeyList
);
}
else
{
writer
->
queue
(
errorMsg
);
}
writer
->
queue
(
QStringLiteral
(
"</div>"
));
}
else
{
if
(
mMetaData
.
isSigned
&&
mVerifiedText
.
isEmpty
()
&&
!
hideErrors
)
{
const
CryptoBlock
block
(
mOtp
,
&
mMetaData
,
mCryptoProto
,
mOtp
->
mSource
,
mFromAddress
,
mNode
);
...
...
mimetreeparser/src/viewer/messagepart.h
View file @
9bb1dd91
...
...
@@ -27,6 +27,7 @@
#include <Libkleo/CryptoBackend>
#include <gpgme++/verificationresult.h>
#include <gpgme++/decryptionresult.h>
#include <importresult.h>
#include <QString>
...
...
@@ -423,13 +424,21 @@ private:
but we're deferring decryption for later. */
void
writeDeferredDecryptionBlock
()
const
;
/** Handles the dectyptioon of a given content
* returns true if the decryption was successfull
* if used in async mode, check if mMetaData.inProgress is true, it inicates a running decryption process.
*/
bool
okDecryptMIME
(
KMime
::
Content
&
data
);
protected:
bool
mPassphraseError
;
bool
mNoSecKey
;
const
Kleo
::
CryptoBackend
::
Protocol
*
mCryptoProto
;
QString
mFromAddress
;
KMime
::
Content
*
mNode
;
bool
mDecryptMessage
;
QByteArray
mVerifiedText
;
std
::
vector
<
GpgME
::
DecryptionResult
::
Recipient
>
mDecryptRecipients
;
};
}
...
...
mimetreeparser/src/viewer/objecttreeparser.cpp
View file @
9bb1dd91
...
...
@@ -39,7 +39,6 @@
#include "memento/verifydetachedbodypartmemento.h"
#include "memento/verifyopaquebodypartmemento.h"
#include "memento/cryptobodypartmemento.h"
#include "memento/decryptverifybodypartmemento.h"
#include "messagepart.h"
#include "objecttreesourceif.h"
...
...
@@ -70,7 +69,6 @@
// KDEPIMLIBS includes
#include <gpgme++/importresult.h>
#include <gpgme++/decryptionresult.h>
#include <gpgme++/key.h>
#include <gpgme++/keylistresult.h>
#include <gpgme.h>
...
...
@@ -565,183 +563,6 @@ void ObjectTreeParser::writeCertificateImportResult(const GpgME::ImportResult &r
htmlWriter
()
->
queue
(
QStringLiteral
(
"<hr/>"
));
}
bool
ObjectTreeParser
::
okDecryptMIME
(
KMime
::
Content
&
data
,
QByteArray
&
decryptedData
,
bool
&
signatureFound
,
std
::
vector
<
GpgME
::
Signature
>
&
signatures
,
bool
showWarning
,
bool
&
passphraseError
,
bool
&
actuallyEncrypted
,
bool
&
decryptionStarted
,
PartMetaData
&
partMetaData
)
{
passphraseError
=
false
;
decryptionStarted
=
false
;
partMetaData
.
errorText
.
clear
();
partMetaData
.
auditLogError
=
GpgME
::
Error
();
partMetaData
.
auditLog
.
clear
();
bool
bDecryptionOk
=
false
;
enum
{
NO_PLUGIN
,
NOT_INITIALIZED
,
CANT_DECRYPT
}
cryptPlugError
=
NO_PLUGIN
;
const
Kleo
::
CryptoBackend
::
Protocol
*
cryptProto
=
cryptoProtocol
();
QString
cryptPlugLibName
;
if
(
cryptProto
)
{
cryptPlugLibName
=
cryptProto
->
name
();
}
assert
(
mSource
->
decryptMessage
());
const
QString
errorMsg
=
i18n
(
"Could not decrypt the data."
);
if
(
cryptProto
)
{
QByteArray
ciphertext
=
data
.
decodedContent
();
#ifdef MARCS_DEBUG
QString
cipherStr
=
QString
::
fromLatin1
(
ciphertext
);
bool
cipherIsBinary
=
(
!
cipherStr
.
contains
(
QStringLiteral
(
"BEGIN ENCRYPTED MESSAGE"
),
Qt
::
CaseInsensitive
))
&&
(
!
cipherStr
.
contains
(
QStringLiteral
(
"BEGIN PGP ENCRYPTED MESSAGE"
),
Qt
::
CaseInsensitive
))
&&
(
!
cipherStr
.
contains
(
QStringLiteral
(
"BEGIN PGP MESSAGE"
),
Qt
::
CaseInsensitive
));
dumpToFile
(
"dat_04_reader.encrypted"
,
ciphertext
.
data
(),
ciphertext
.
size
());
QString
deb
;
deb
=
QLatin1String
(
"
\n\n
E N C R Y P T E D D A T A = "
);
if
(
cipherIsBinary
)
{
deb
+=
QLatin1String
(
"[binary data]"
);
}
else
{
deb
+=
QLatin1String
(
"
\"
"
);
deb
+=
cipherStr
;
deb
+=
QLatin1String
(
"
\"
"
);
}
deb
+=
"
\n\n
"
;
qCDebug
(
MIMETREEPARSER_LOG
)
<<
deb
;
#endif
qCDebug
(
MIMETREEPARSER_LOG
)
<<
"going to call CRYPTPLUG"
<<
cryptPlugLibName
;
// Check whether the memento contains a result from last time:
const
DecryptVerifyBodyPartMemento
*
m
=
dynamic_cast
<
DecryptVerifyBodyPartMemento
*>
(
mNodeHelper
->
bodyPartMemento
(
&
data
,
"decryptverify"
));
if
(
!
m
)
{
Kleo
::
DecryptVerifyJob
*
job
=
cryptProto
->
decryptVerifyJob
();
if
(
!
job
)
{
cryptPlugError
=
CANT_DECRYPT
;
cryptProto
=
0
;
}
else
{
DecryptVerifyBodyPartMemento
*
newM
=
new
DecryptVerifyBodyPartMemento
(
job
,
ciphertext
);
if
(
allowAsync
())
{
QObject
::
connect
(
newM
,
&
CryptoBodyPartMemento
::
update
,
nodeHelper
(),
&
NodeHelper
::
update
);
QObject
::
connect
(
newM
,
SIGNAL
(
update
(
MimeTreeParser
::
UpdateMode
)),
mSource
->
sourceObject
(),
SLOT
(
update
(
MimeTreeParser
::
UpdateMode
)));
if
(
newM
->
start
())
{
decryptionStarted
=
true
;
mHasPendingAsyncJobs
=
true
;
}
else
{
m
=
newM
;
}
}
else
{
newM
->
exec
();
m
=
newM
;
}
mNodeHelper
->
setBodyPartMemento
(
&
data
,
"decryptverify"
,
newM
);
}
}
else
if
(
m
->
isRunning
())
{
decryptionStarted
=
true
;
mHasPendingAsyncJobs
=
true
;
m
=
0
;
}
if
(
m
)
{
const
QByteArray
&
plainText
=
m
->
plainText
();
const
GpgME
::
DecryptionResult
&
decryptResult
=
m
->
decryptResult
();
const
GpgME
::
VerificationResult
&
verifyResult
=
m
->
verifyResult
();
std
::
stringstream
ss
;
ss
<<
decryptResult
<<
'\n'
<<
verifyResult
;
qCDebug
(
MIMETREEPARSER_LOG
)
<<
ss
.
str
().
c_str
();
signatureFound
=
verifyResult
.
signatures
().
size
()
>
0
;
signatures
=
verifyResult
.
signatures
();
bDecryptionOk
=
!
decryptResult
.
error
();
partMetaData
.
auditLogError
=
m
->
auditLogError
();
partMetaData
.
auditLog
=
m
->
auditLogAsHtml
();
if
(
!
bDecryptionOk
&&
signatureFound
)
{
//Only a signed part
actuallyEncrypted
=
false
;
bDecryptionOk
=
true
;
decryptedData
=
plainText
;
}
else
{
passphraseError
=
decryptResult
.
error
().
isCanceled
()
||
decryptResult
.
error
().
code
()
==
GPG_ERR_NO_SECKEY
;
actuallyEncrypted
=
decryptResult
.
error
().
code
()
!=
GPG_ERR_NO_DATA
;
partMetaData
.
errorText
=
QString
::
fromLocal8Bit
(
decryptResult
.
error
().
asString
());
partMetaData
.
isEncrypted
=
actuallyEncrypted
;
if
(
actuallyEncrypted
&&
decryptResult
.
numRecipients
()
>
0
)
{
partMetaData
.
keyId
=
decryptResult
.
recipient
(
0
).
keyID
();
}
qCDebug
(
MIMETREEPARSER_LOG
)
<<
"ObjectTreeParser::decryptMIME: returned from CRYPTPLUG"
;
if
(
bDecryptionOk
)
{
decryptedData
=
plainText
;
}
else
if
(
htmlWriter
()
&&
showWarning
)
{
bool
noSecKey
=
true
;
const
QString
sNoSecKeyHeader
=
i18n
(
"No secret key found to encrypt the message. It is encrypted for following keys:"
);
QString
secKeyList
;
foreach
(
const
GpgME
::
DecryptionResult
::
Recipient
&
recipient
,
decryptResult
.
recipients
())
{
noSecKey
&=
(
recipient
.
status
().
code
()
==
GPG_ERR_NO_SECKEY
);
if
(
!
secKeyList
.
isEmpty
())
{
secKeyList
+=
QStringLiteral
(
"<br />"
);
}
secKeyList
+=
QStringLiteral
(
"<a href=
\"
kmail:showCertificate#%1 ### %2 ### %3
\"
>%4</a>"
)
.
arg
(
cryptProto
->
displayName
(),
cryptProto
->
name
(),
QString
::
fromLatin1
(
recipient
.
keyID
()),
QString
::
fromLatin1
(
QByteArray
(
"0x"
)
+
recipient
.
keyID
())
);
}
decryptedData
=
"<div style=
\"
font-size:x-large; text-align:center; padding:20pt;
\"
>"
;
if
(
noSecKey
)
{
decryptedData
+=
QString
(
sNoSecKeyHeader
+
QStringLiteral
(
"<br />"
)
+
secKeyList
).
toUtf8
();
}
else
{
decryptedData
+=
errorMsg
.
toUtf8
();
}
decryptedData
+=
"</div>"
;
if
(
!
passphraseError
)
{
partMetaData
.
errorText
=
i18n
(
"Crypto plug-in
\"
%1
\"
could not decrypt the data."
,
cryptPlugLibName
)
+
QLatin1String
(
"<br />"
)
+
i18n
(
"Error: %1"
,
partMetaData
.
errorText
);
}
}
}
}
}
if
(
!
cryptProto
)
{
decryptedData
=
"<div style=
\"
text-align:center; padding:20pt;
\"
>"
+
errorMsg
.
toUtf8
()
+
"</div>"
;
switch
(
cryptPlugError
)
{
case
NOT_INITIALIZED
:
partMetaData
.
errorText
=
i18n
(
"Crypto plug-in
\"
%1
\"
is not initialized."
,
cryptPlugLibName
);
break
;
case
CANT_DECRYPT
:
partMetaData
.
errorText
=
i18n
(
"Crypto plug-in
\"
%1
\"
cannot decrypt messages."
,
cryptPlugLibName
);
break
;
case
NO_PLUGIN
:
partMetaData
.
errorText
=
i18n
(
"No appropriate crypto plug-in was found."
);
break
;
}
}
dumpToFile
(
"dat_05_reader.decrypted"
,
decryptedData
.
data
(),
decryptedData
.
size
());
return
bDecryptionOk
;
}
MessagePart
::
Ptr
ObjectTreeParser
::
processTextHtmlSubtype
(
KMime
::
Content
*
curNode
,
ProcessResult
&
)
{
HtmlMessagePart
::
Ptr
mp
(
new
HtmlMessagePart
(
this
,
curNode
,
mSource
));
...
...
mimetreeparser/src/viewer/objecttreeparser.h
View file @
9bb1dd91
...
...
@@ -360,18 +360,6 @@ private:
/** Writes out the information contained in a GpgME::ImportResult */
void
writeCertificateImportResult
(
const
GpgME
::
ImportResult
&
res
);
/** Returns the contents of the given multipart/encrypted
object. Data is decypted. May contain body parts. */
bool
okDecryptMIME
(
KMime
::
Content
&
data
,
QByteArray
&
decryptedData
,
bool
&
signatureFound
,
std
::
vector
<
GpgME
::
Signature
>
&
signatures
,
bool
showWarning
,
bool
&
passphraseError
,
bool
&
actuallyEncrypted
,
bool
&
decryptionStarted
,
PartMetaData
&
partMetaData
);
bool
okVerify
(
const
QByteArray
&
data
,
const
Kleo
::
CryptoBackend
::
Protocol
*
cryptProto
,
MimeTreeParser
::
PartMetaData
&
messagePart
,
QByteArray
&
verifiedText
,
std
::
vector
<
GpgME
::
Signature
>
&
signatures
,
const
QByteArray
&
signature
,
KMime
::
Content
*
sign
);
void
sigStatusToMetaData
(
const
std
::
vector
<
GpgME
::
Signature
>
&
signatures
,
const
Kleo
::
CryptoBackend
::
Protocol
*
cryptoProtocol
,
PartMetaData
&
messagePart
,
GpgME
::
Key
key
);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment