]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/textsecure/Main.java
4227e65ede663a3796964f7b3c3d5fcd8f4b7368
[signal-cli] / src / main / java / org / asamk / textsecure / Main.java
1 /**
2 * Copyright (C) 2015 AsamK
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17 package org.asamk.textsecure;
18
19 import net.sourceforge.argparse4j.ArgumentParsers;
20 import net.sourceforge.argparse4j.impl.Arguments;
21 import net.sourceforge.argparse4j.inf.*;
22 import org.apache.commons.io.IOUtils;
23 import org.apache.http.util.TextUtils;
24 import org.asamk.TextSecure;
25 import org.freedesktop.dbus.DBusConnection;
26 import org.freedesktop.dbus.exceptions.DBusException;
27 import org.freedesktop.dbus.exceptions.DBusExecutionException;
28 import org.whispersystems.textsecure.api.crypto.UntrustedIdentityException;
29 import org.whispersystems.textsecure.api.messages.*;
30 import org.whispersystems.textsecure.api.messages.multidevice.TextSecureSyncMessage;
31 import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions;
32 import org.whispersystems.textsecure.api.push.exceptions.NetworkFailureException;
33 import org.whispersystems.textsecure.api.push.exceptions.UnregisteredUserException;
34 import org.whispersystems.textsecure.api.util.PhoneNumberFormatter;
35
36 import java.io.File;
37 import java.io.IOException;
38 import java.security.Security;
39 import java.util.ArrayList;
40 import java.util.List;
41
42 public class Main {
43
44 public static final String TEXTSECURE_BUSNAME = "org.asamk.TextSecure";
45 public static final String TEXTSECURE_OBJECTPATH = "/org/asamk/TextSecure";
46
47 public static void main(String[] args) {
48 // Workaround for BKS truststore
49 Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
50
51 Namespace ns = parseArgs(args);
52 if (ns == null) {
53 System.exit(1);
54 }
55
56 final String username = ns.getString("username");
57 Manager m;
58 TextSecure ts;
59 DBusConnection dBusConn = null;
60 try {
61 if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) {
62 try {
63 m = null;
64 int busType;
65 if (ns.getBoolean("dbus_system")) {
66 busType = DBusConnection.SYSTEM;
67 } else {
68 busType = DBusConnection.SESSION;
69 }
70 dBusConn = DBusConnection.getConnection(busType);
71 ts = (TextSecure) dBusConn.getRemoteObject(
72 TEXTSECURE_BUSNAME, TEXTSECURE_OBJECTPATH,
73 TextSecure.class);
74 } catch (DBusException e) {
75 e.printStackTrace();
76 if (dBusConn != null) {
77 dBusConn.disconnect();
78 }
79 System.exit(3);
80 return;
81 }
82 } else {
83 String settingsPath = ns.getString("config");
84 if (TextUtils.isEmpty(settingsPath)) {
85 settingsPath = System.getProperty("user.home") + "/.config/textsecure";
86 }
87
88 m = new Manager(username, settingsPath);
89 ts = m;
90 if (m.userExists()) {
91 try {
92 m.load();
93 } catch (Exception e) {
94 System.err.println("Error loading state file \"" + m.getFileName() + "\": " + e.getMessage());
95 System.exit(2);
96 return;
97 }
98 }
99 }
100
101 switch (ns.getString("command")) {
102 case "register":
103 if (dBusConn != null) {
104 System.err.println("register is not yet implementd via dbus");
105 System.exit(1);
106 }
107 if (!m.userHasKeys()) {
108 m.createNewIdentity();
109 }
110 try {
111 m.register(ns.getBoolean("voice"));
112 } catch (IOException e) {
113 System.err.println("Request verify error: " + e.getMessage());
114 System.exit(3);
115 }
116 break;
117 case "verify":
118 if (dBusConn != null) {
119 System.err.println("verify is not yet implementd via dbus");
120 System.exit(1);
121 }
122 if (!m.userHasKeys()) {
123 System.err.println("User has no keys, first call register.");
124 System.exit(1);
125 }
126 if (m.isRegistered()) {
127 System.err.println("User registration is already verified");
128 System.exit(1);
129 }
130 try {
131 m.verifyAccount(ns.getString("verificationCode"));
132 } catch (IOException e) {
133 System.err.println("Verify error: " + e.getMessage());
134 System.exit(3);
135 }
136 break;
137 case "send":
138 if (dBusConn == null && !m.isRegistered()) {
139 System.err.println("User is not registered.");
140 System.exit(1);
141 }
142
143 if (ns.getBoolean("endsession")) {
144 if (ns.getList("recipient") == null) {
145 System.err.println("No recipients given");
146 System.err.println("Aborting sending.");
147 System.exit(1);
148 }
149 try {
150 ts.sendEndSessionMessage(ns.<String>getList("recipient"));
151 } catch (IOException e) {
152 handleIOException(e);
153 } catch (EncapsulatedExceptions e) {
154 handleEncapsulatedExceptions(e);
155 } catch (AssertionError e) {
156 handleAssertionError(e);
157 } catch (DBusExecutionException e) {
158 handleDBusExecutionException(e);
159 }
160 } else {
161 String messageText = ns.getString("message");
162 if (messageText == null) {
163 try {
164 messageText = IOUtils.toString(System.in);
165 } catch (IOException e) {
166 System.err.println("Failed to read message from stdin: " + e.getMessage());
167 System.err.println("Aborting sending.");
168 System.exit(1);
169 }
170 }
171
172 try {
173 List<String> attachments = ns.getList("attachment");
174 if (attachments == null) {
175 attachments = new ArrayList<>();
176 }
177 if (ns.getString("group") != null) {
178 byte[] groupId = decodeGroupId(ns.getString("group"));
179 ts.sendGroupMessage(messageText, attachments, groupId);
180 } else {
181 ts.sendMessage(messageText, attachments, ns.<String>getList("recipient"));
182 }
183 } catch (IOException e) {
184 handleIOException(e);
185 } catch (EncapsulatedExceptions e) {
186 handleEncapsulatedExceptions(e);
187 } catch (AssertionError e) {
188 handleAssertionError(e);
189 } catch (GroupNotFoundException e) {
190 handleGroupNotFoundException(e);
191 } catch (AttachmentInvalidException e) {
192 System.err.println("Failed to add attachment: " + e.getMessage());
193 System.err.println("Aborting sending.");
194 System.exit(1);
195 } catch (DBusExecutionException e) {
196 handleDBusExecutionException(e);
197 }
198 }
199
200 break;
201 case "receive":
202 if (dBusConn != null) {
203 System.err.println("receive is not yet implementd via dbus");
204 System.exit(1);
205 }
206 if (!m.isRegistered()) {
207 System.err.println("User is not registered.");
208 System.exit(1);
209 }
210 int timeout = 5;
211 if (ns.getInt("timeout") != null) {
212 timeout = ns.getInt("timeout");
213 }
214 boolean returnOnTimeout = true;
215 if (timeout < 0) {
216 returnOnTimeout = false;
217 timeout = 3600;
218 }
219 try {
220 m.receiveMessages(timeout, returnOnTimeout, new ReceiveMessageHandler(m));
221 } catch (IOException e) {
222 System.err.println("Error while receiving messages: " + e.getMessage());
223 System.exit(3);
224 } catch (AssertionError e) {
225 handleAssertionError(e);
226 }
227 break;
228 case "quitGroup":
229 if (dBusConn != null) {
230 System.err.println("quitGroup is not yet implementd via dbus");
231 System.exit(1);
232 }
233 if (!m.isRegistered()) {
234 System.err.println("User is not registered.");
235 System.exit(1);
236 }
237
238 try {
239 m.sendQuitGroupMessage(decodeGroupId(ns.getString("group")));
240 } catch (IOException e) {
241 handleIOException(e);
242 } catch (EncapsulatedExceptions e) {
243 handleEncapsulatedExceptions(e);
244 } catch (AssertionError e) {
245 handleAssertionError(e);
246 } catch (GroupNotFoundException e) {
247 handleGroupNotFoundException(e);
248 }
249
250 break;
251 case "updateGroup":
252 if (dBusConn != null) {
253 System.err.println("updateGroup is not yet implementd via dbus");
254 System.exit(1);
255 }
256 if (!m.isRegistered()) {
257 System.err.println("User is not registered.");
258 System.exit(1);
259 }
260
261 try {
262 byte[] groupId = null;
263 if (ns.getString("group") != null) {
264 groupId = decodeGroupId(ns.getString("group"));
265 }
266 byte[] newGroupId = m.sendUpdateGroupMessage(groupId, ns.getString("name"), ns.<String>getList("member"), ns.getString("avatar"));
267 if (groupId == null) {
268 System.out.println("Creating new group \"" + Base64.encodeBytes(newGroupId) + "\" …");
269 }
270 } catch (IOException e) {
271 handleIOException(e);
272 } catch (AttachmentInvalidException e) {
273 System.err.println("Failed to add avatar attachment for group\": " + e.getMessage());
274 System.err.println("Aborting sending.");
275 System.exit(1);
276 } catch (GroupNotFoundException e) {
277 handleGroupNotFoundException(e);
278 } catch (EncapsulatedExceptions e) {
279 handleEncapsulatedExceptions(e);
280 }
281
282 break;
283 case "daemon":
284 if (dBusConn != null) {
285 System.err.println("Stop it.");
286 System.exit(1);
287 }
288 if (!m.isRegistered()) {
289 System.err.println("User is not registered.");
290 System.exit(1);
291 }
292 DBusConnection conn = null;
293 try {
294 try {
295 int busType;
296 if (ns.getBoolean("system")) {
297 busType = DBusConnection.SYSTEM;
298 } else {
299 busType = DBusConnection.SESSION;
300 }
301 conn = DBusConnection.getConnection(busType);
302 conn.exportObject(TEXTSECURE_OBJECTPATH, m);
303 conn.requestBusName(TEXTSECURE_BUSNAME);
304 } catch (DBusException e) {
305 e.printStackTrace();
306 System.exit(3);
307 }
308 try {
309 m.receiveMessages(3600, false, new DbusReceiveMessageHandler(m, conn));
310 } catch (IOException e) {
311 System.err.println("Error while receiving messages: " + e.getMessage());
312 System.exit(3);
313 } catch (AssertionError e) {
314 handleAssertionError(e);
315 }
316 } finally {
317 if (conn != null) {
318 conn.disconnect();
319 }
320 }
321
322 break;
323 }
324 System.exit(0);
325 } finally {
326 if (dBusConn != null) {
327 dBusConn.disconnect();
328 }
329 }
330 }
331
332 private static void handleGroupNotFoundException(GroupNotFoundException e) {
333 System.err.println("Failed to send to group: " + e.getMessage());
334 System.err.println("Aborting sending.");
335 System.exit(1);
336 }
337
338 private static void handleDBusExecutionException(DBusExecutionException e) {
339 System.err.println("Cannot connect to dbus: " + e.getMessage());
340 System.err.println("Aborting.");
341 System.exit(1);
342 }
343
344 private static byte[] decodeGroupId(String groupId) {
345 try {
346 return Base64.decode(groupId);
347 } catch (IOException e) {
348 System.err.println("Failed to decode groupId (must be base64) \"" + groupId + "\": " + e.getMessage());
349 System.err.println("Aborting sending.");
350 System.exit(1);
351 return null;
352 }
353 }
354
355 private static Namespace parseArgs(String[] args) {
356 ArgumentParser parser = ArgumentParsers.newArgumentParser("textsecure-cli")
357 .defaultHelp(true)
358 .description("Commandline interface for TextSecure.")
359 .version(Manager.PROJECT_NAME + " " + Manager.PROJECT_VERSION);
360
361 parser.addArgument("-v", "--version")
362 .help("Show package version.")
363 .action(Arguments.version());
364 parser.addArgument("--config")
365 .help("Set the path, where to store the config (Default: $HOME/.config/textsecure-cli).");
366
367 MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup();
368 mut.addArgument("-u", "--username")
369 .help("Specify your phone number, that will be used for verification.");
370 mut.addArgument("--dbus")
371 .help("Make request via user dbus.")
372 .action(Arguments.storeTrue());
373 mut.addArgument("--dbus-system")
374 .help("Make request via system dbus.")
375 .action(Arguments.storeTrue());
376
377 Subparsers subparsers = parser.addSubparsers()
378 .title("subcommands")
379 .dest("command")
380 .description("valid subcommands")
381 .help("additional help");
382
383 Subparser parserRegister = subparsers.addParser("register");
384 parserRegister.addArgument("-v", "--voice")
385 .help("The verification should be done over voice, not sms.")
386 .action(Arguments.storeTrue());
387
388 Subparser parserVerify = subparsers.addParser("verify");
389 parserVerify.addArgument("verificationCode")
390 .help("The verification code you received via sms or voice call.");
391
392 Subparser parserSend = subparsers.addParser("send");
393 parserSend.addArgument("-g", "--group")
394 .help("Specify the recipient group ID.");
395 parserSend.addArgument("recipient")
396 .help("Specify the recipients' phone number.")
397 .nargs("*");
398 parserSend.addArgument("-m", "--message")
399 .help("Specify the message, if missing standard input is used.");
400 parserSend.addArgument("-a", "--attachment")
401 .nargs("*")
402 .help("Add file as attachment");
403 parserSend.addArgument("-e", "--endsession")
404 .help("Clear session state and send end session message.")
405 .action(Arguments.storeTrue());
406
407 Subparser parserLeaveGroup = subparsers.addParser("quitGroup");
408 parserLeaveGroup.addArgument("-g", "--group")
409 .required(true)
410 .help("Specify the recipient group ID.");
411
412 Subparser parserUpdateGroup = subparsers.addParser("updateGroup");
413 parserUpdateGroup.addArgument("-g", "--group")
414 .help("Specify the recipient group ID.");
415 parserUpdateGroup.addArgument("-n", "--name")
416 .help("Specify the new group name.");
417 parserUpdateGroup.addArgument("-a", "--avatar")
418 .help("Specify a new group avatar image file");
419 parserUpdateGroup.addArgument("-m", "--member")
420 .nargs("*")
421 .help("Specify one or more members to add to the group");
422
423 Subparser parserReceive = subparsers.addParser("receive");
424 parserReceive.addArgument("-t", "--timeout")
425 .type(int.class)
426 .help("Number of seconds to wait for new messages (negative values disable timeout)");
427
428 Subparser parserDaemon = subparsers.addParser("daemon");
429 parserDaemon.addArgument("--system")
430 .action(Arguments.storeTrue())
431 .help("Use DBus system bus instead of user bus.");
432
433 try {
434 Namespace ns = parser.parseArgs(args);
435 if (!ns.getBoolean("dbus") && !ns.getBoolean("dbus_system")) {
436 if (ns.getString("username") == null) {
437 parser.printUsage();
438 System.err.println("You need to specify a username (phone number)");
439 System.exit(2);
440 }
441 if (!PhoneNumberFormatter.isValidNumber(ns.getString("username"))) {
442 System.err.println("Invalid username (phone number), make sure you include the country code.");
443 System.exit(2);
444 }
445 }
446 if (ns.getList("recipient") != null && !ns.getList("recipient").isEmpty() && ns.getString("group") != null) {
447 System.err.println("You cannot specify recipients by phone number and groups a the same time");
448 System.exit(2);
449 }
450 return ns;
451 } catch (ArgumentParserException e) {
452 parser.handleError(e);
453 return null;
454 }
455 }
456
457 private static void handleAssertionError(AssertionError e) {
458 System.err.println("Failed to send/receive message (Assertion): " + e.getMessage());
459 System.err.println(e.getStackTrace());
460 System.err.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README");
461 System.exit(1);
462 }
463
464 private static void handleEncapsulatedExceptions(EncapsulatedExceptions e) {
465 System.err.println("Failed to send (some) messages:");
466 for (NetworkFailureException n : e.getNetworkExceptions()) {
467 System.err.println("Network failure for \"" + n.getE164number() + "\": " + n.getMessage());
468 }
469 for (UnregisteredUserException n : e.getUnregisteredUserExceptions()) {
470 System.err.println("Unregistered user \"" + n.getE164Number() + "\": " + n.getMessage());
471 }
472 for (UntrustedIdentityException n : e.getUntrustedIdentityExceptions()) {
473 System.err.println("Untrusted Identity for \"" + n.getE164Number() + "\": " + n.getMessage());
474 }
475 }
476
477 private static void handleIOException(IOException e) {
478 System.err.println("Failed to send message: " + e.getMessage());
479 }
480
481 private static class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
482 final Manager m;
483
484 public ReceiveMessageHandler(Manager m) {
485 this.m = m;
486 }
487
488 @Override
489 public void handleMessage(TextSecureEnvelope envelope, TextSecureContent content, GroupInfo group) {
490 System.out.println("Envelope from: " + envelope.getSource());
491 System.out.println("Timestamp: " + envelope.getTimestamp());
492
493 if (envelope.isReceipt()) {
494 System.out.println("Got receipt.");
495 } else if (envelope.isWhisperMessage() | envelope.isPreKeyWhisperMessage()) {
496 if (content == null) {
497 System.out.println("Failed to decrypt message.");
498 } else {
499 if (content.getDataMessage().isPresent()) {
500 TextSecureDataMessage message = content.getDataMessage().get();
501
502 System.out.println("Message timestamp: " + message.getTimestamp());
503
504 if (message.getBody().isPresent()) {
505 System.out.println("Body: " + message.getBody().get());
506 }
507 if (message.getGroupInfo().isPresent()) {
508 TextSecureGroup groupInfo = message.getGroupInfo().get();
509 System.out.println("Group info:");
510 System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId()));
511 if (groupInfo.getName().isPresent()) {
512 System.out.println(" Name: " + groupInfo.getName().get());
513 } else if (group != null) {
514 System.out.println(" Name: " + group.name);
515 } else {
516 System.out.println(" Name: <Unknown group>");
517 }
518 System.out.println(" Type: " + groupInfo.getType());
519 if (groupInfo.getMembers().isPresent()) {
520 for (String member : groupInfo.getMembers().get()) {
521 System.out.println(" Member: " + member);
522 }
523 }
524 if (groupInfo.getAvatar().isPresent()) {
525 System.out.println(" Avatar:");
526 printAttachment(groupInfo.getAvatar().get());
527 }
528 }
529 if (message.isEndSession()) {
530 System.out.println("Is end session");
531 }
532
533 if (message.getAttachments().isPresent()) {
534 System.out.println("Attachments: ");
535 for (TextSecureAttachment attachment : message.getAttachments().get()) {
536 printAttachment(attachment);
537 }
538 }
539 }
540 if (content.getSyncMessage().isPresent()) {
541 TextSecureSyncMessage syncMessage = content.getSyncMessage().get();
542 System.out.println("Received sync message");
543 }
544 }
545 } else {
546 System.out.println("Unknown message received.");
547 }
548 System.out.println();
549 }
550
551 private void printAttachment(TextSecureAttachment attachment) {
552 System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")");
553 if (attachment.isPointer()) {
554 final TextSecureAttachmentPointer pointer = attachment.asPointer();
555 System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length + (pointer.getRelay().isPresent() ? " Relay: " + pointer.getRelay().get() : ""));
556 System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "<unavailable>") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : ""));
557 File file = m.getAttachmentFile(pointer.getId());
558 if (file.exists()) {
559 System.out.println(" Stored plaintext in: " + file);
560 }
561 }
562 }
563 }
564
565 private static class DbusReceiveMessageHandler implements Manager.ReceiveMessageHandler {
566 final Manager m;
567 final DBusConnection conn;
568
569 public DbusReceiveMessageHandler(Manager m, DBusConnection conn) {
570 this.m = m;
571 this.conn = conn;
572 }
573
574 @Override
575 public void handleMessage(TextSecureEnvelope envelope, TextSecureContent content, GroupInfo group) {
576 System.out.println("Envelope from: " + envelope.getSource());
577 System.out.println("Timestamp: " + envelope.getTimestamp());
578
579 if (envelope.isReceipt()) {
580 System.out.println("Got receipt.");
581 } else if (envelope.isWhisperMessage() | envelope.isPreKeyWhisperMessage()) {
582 if (content == null) {
583 System.out.println("Failed to decrypt message.");
584 } else {
585 if (content.getDataMessage().isPresent()) {
586 TextSecureDataMessage message = content.getDataMessage().get();
587
588 System.out.println("Message timestamp: " + message.getTimestamp());
589
590 if (message.getBody().isPresent()) {
591 System.out.println("Body: " + message.getBody().get());
592 }
593
594 if (message.getGroupInfo().isPresent()) {
595 TextSecureGroup groupInfo = message.getGroupInfo().get();
596 System.out.println("Group info:");
597 System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId()));
598 if (groupInfo.getName().isPresent()) {
599 System.out.println(" Name: " + groupInfo.getName().get());
600 } else if (group != null) {
601 System.out.println(" Name: " + group.name);
602 } else {
603 System.out.println(" Name: <Unknown group>");
604 }
605 System.out.println(" Type: " + groupInfo.getType());
606 if (groupInfo.getMembers().isPresent()) {
607 for (String member : groupInfo.getMembers().get()) {
608 System.out.println(" Member: " + member);
609 }
610 }
611 if (groupInfo.getAvatar().isPresent()) {
612 System.out.println(" Avatar:");
613 printAttachment(groupInfo.getAvatar().get());
614 }
615 }
616 if (message.isEndSession()) {
617 System.out.println("Is end session");
618 }
619
620 List<String> attachments = new ArrayList<>();
621 if (message.getAttachments().isPresent()) {
622 System.out.println("Attachments: ");
623 for (TextSecureAttachment attachment : message.getAttachments().get()) {
624 if (attachment.isPointer()) {
625 attachments.add(m.getAttachmentFile(attachment.asPointer().getId()).getAbsolutePath());
626 }
627 printAttachment(attachment);
628 }
629 }
630 if (!message.isEndSession() &&
631 !(message.getGroupInfo().isPresent() && message.getGroupInfo().get().getType() != TextSecureGroup.Type.DELIVER)) {
632 try {
633 conn.sendSignal(new TextSecure.MessageReceived(
634 TEXTSECURE_OBJECTPATH,
635 envelope.getSource(),
636 message.getGroupInfo().isPresent() ? message.getGroupInfo().get().getGroupId() : new byte[0],
637 message.getBody().isPresent() ? message.getBody().get() : "",
638 attachments));
639 } catch (DBusException e) {
640 e.printStackTrace();
641 }
642 }
643 }
644 if (content.getSyncMessage().isPresent()) {
645 TextSecureSyncMessage syncMessage = content.getSyncMessage().get();
646 System.out.println("Received sync message");
647 }
648 }
649 } else {
650 System.out.println("Unknown message received.");
651 }
652 System.out.println();
653 }
654
655 private void printAttachment(TextSecureAttachment attachment) {
656 System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")");
657 if (attachment.isPointer()) {
658 final TextSecureAttachmentPointer pointer = attachment.asPointer();
659 System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length + (pointer.getRelay().isPresent() ? " Relay: " + pointer.getRelay().get() : ""));
660 System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "<unavailable>") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : ""));
661 File file = m.getAttachmentFile(pointer.getId());
662 if (file.exists()) {
663 System.out.println(" Stored plaintext in: " + file);
664 }
665 }
666 }
667 }
668 }