Java Java, E-Mails lesen

CyborgBeta

Commander
Registriert
Jan. 2021
Beiträge
2.516
Hi, ich hab den folgenden Code, um E-Mails mehrerer Accounts zu lesen und darzustellen:

Java:
import jakarta.mail.*;
import jakarta.mail.internet.MimeMultipart;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.CyclicBarrier;
import javax.swing.*;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Safelist;

public class Main {
  record Account(
      int index, String host, int port, String user, String password, String inboxName) {}

  private static final List<Account> accounts =
      List.of(
          new Account(1, "", 993, "", "", "INBOX"),
          new Account(2, "", 993, "", "", "INBOX"));
  private static final Map<Account, List<Message>> messages = new LinkedHashMap<>();
  private static final int max_mails = 5;
  private static volatile int state = 1;

  private static final CyclicBarrier barrier =
      new CyclicBarrier(
          accounts.size(),
          () -> {
            try {
              int i1 = 1;
              List<Message> tempList = new ArrayList<>();
              for (Map.Entry<Account, List<Message>> e : messages.entrySet()) {
                System.out.println(e.getKey().index() + " " + e.getKey().host() + ":");
                for (Message m : e.getValue()) {
                  System.out.println(
                      i1++ + " " + getDate(m) + " " + getFrom(m) + " " + getSubject(m));
                  // System.out.println(getContent(m).lines().findFirst().orElse(""));
                  tempList.add(m);
                }
                System.out.println();
              }
              System.out.println("a or a-b or empty or r or all:");
              while (true) {
                String line = new Scanner(System.in, StandardCharsets.UTF_8).nextLine();
                if (line == null || line.isBlank()) {
                  state = 0;
                  return;
                }
                if (line.equals("r")) {
                  state = 1;
                  return;
                }
                if (line.equals("all")) {
                  for (Message m : tempList) {
                    display1(m);
                  }
                  continue;
                }
                if (line.contains("-")) {
                  int a = Integer.parseInt(line.split("-")[0]);
                  int b = Integer.parseInt(line.split("-")[1]);
                  for (int i = a; i <= b; i++) {
                    display1(tempList.get(i - 1));
                  }
                  continue;
                }
                int i2 = Integer.parseInt(line);
                display1(tempList.get(i2 - 1));
              }
            } catch (Exception e) {
              throw new RuntimeException(e);
            }
          });

  public static void main(String[] args) {
    for (Account ac : accounts) {
      new Thread(
              () -> {
                try {
                  receive10(ac);
                } catch (Exception e) {
                  throw new RuntimeException(e);
                }
              })
          .start();
    }
  }

  private static void receive10(Account ac) throws Exception {
    Store emailStore = null;
    Folder emailFolder = null;

    Properties properties = new Properties();
    properties.put("mail.store.protocol", "imap");
    properties.put("mail.imap.ssl.enable", true);
    properties.put("mail.imap.host", ac.host());
    properties.put("mail.imap.port", ac.port());
    Session emailSession = Session.getInstance(properties);

    try {
      emailStore = emailSession.getStore();
      emailStore.connect(ac.user(), ac.password());
      System.out.println("Connected: " + ac.host());

      emailFolder = emailStore.getFolder(ac.inboxName());
      emailFolder.open(Folder.READ_ONLY);

      while (state != 0) {
        System.out.println("Reading: " + ac.host());
        List<Message> l = new ArrayList<>();
        int c1 = emailFolder.getMessageCount();
        int c2 = 0;
        for (int i = c1; i > 0 && c2 < max_mails; i--, c2++) {
          l.add(emailFolder.getMessage(i));
        }
        messages.put(ac, l);
        barrier.await();
      }
    } finally {
      if (emailFolder != null && emailFolder.isOpen()) {
        emailFolder.close(false);
      }
      if (emailStore != null && emailStore.isConnected()) {
        emailStore.close();
      }
      System.out.println("Disconnected: " + ac.host());
    }
  }

  private static void display1(Message message) throws Exception {
    String sb =
        getDate(message)
            + "\n\n"
            + getFrom(message)
            + "\n\n"
            + getSubject(message)
            + "\n\n"
            + getContent(message);
    JFrame f = new JFrame();
    JTextArea ta = new JTextArea(sb);
    ta.setLineWrap(true);
    f.getContentPane().add(new JScrollPane(ta));
    f.setSize(600, 400);
    f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    f.setVisible(true);
  }

  private static String getDate(Message message) throws Exception {
    return message.getSentDate().toString();
  }

  private static String getFrom(Message message) throws Exception {
    StringBuilder sb = new StringBuilder();
    for (Address a : message.getFrom()) {
      sb.append(a.toString());
    }
    return sb.toString();
  }

  private static String getSubject(Message message) throws Exception {
    return message.getSubject();
  }

  private static String getContent(Message message) throws Exception {
    StringBuilder sb = new StringBuilder();
    if (message.isMimeType("text/plain")) {
      sb.append(message.getContent().toString());
    }
    if (message.isMimeType("multipart/*")) {
      MimeMultipart mimeMultipart = (MimeMultipart) message.getContent();
      for (int i = 0; i < mimeMultipart.getCount(); i++) {
        Document document = Jsoup.parse(mimeMultipart.getBodyPart(i).getContent().toString());
        document.outputSettings(new Document.OutputSettings().prettyPrint(false));
        document.select("br").append("\\n");
        document.select("p").prepend("\\n\\n");
        String s = document.html().replaceAll("\\\\n", "\n");
        sb.append(
            Jsoup.clean(s, "", Safelist.none(), new Document.OutputSettings().prettyPrint(false)));
      }
    }
    return sb.toString();
  }
}

Dependencies:

Code:
dependencies {
    // https://mvnrepository.com/artifact/com.sun.mail/jakarta.mail
    implementation 'com.sun.mail:jakarta.mail:2.0.1'
    // https://mvnrepository.com/artifact/org.jsoup/jsoup
    implementation 'org.jsoup:jsoup:1.18.1'
}

Ich möchte den gerne de-synchronisieren, es soll also alles sequenziell, anstatt parallel ablaufen. Wie bekomme ich denn am besten die CyclicBarrier "weg"?

Danke
 
tollertyp schrieb:
Ach es geht um CyclicBarrier?
Ja. Wenn ich keine Threads anlege, dann verhindert die CyclicBarrier natürlich, dass der zweite Account abgerufen wird...

Suche ein Refactoring, das möglichst minimal wäre...
 
Also müsste nicht einfach die Erzeugung der Threads entfernt und die CyclicBarrier entfernt werden?

Alles sequentiell = keine Threads.
 
  • Gefällt mir
Reaktionen: CyborgBeta
Wenn ich beides entferne, dann wurden noch nicht alle E-Mails abgerufen, wenn ich das Menü darstelle ...
 
Also "Entfernen" heißt, dass der Code dann nach der Schleife ohne Threads ausgeführt werden müsste - meiner Meinung nach.
Ansonsten: Wenn du keine Parallele Ausführung willst, einen ThreadPool mit einem Thread machen, und die Threads dort laufen lassen?

Also dein konkretes Anliegen ist mir halt noch nicht klar, was du willst.

P.S. Der Thread-Titel ist nicht sinnvoll, mit E-Mails lesen hat das eigentlich wenig zu tun. Geht ja darum dass Parallele Ausführung in Sequentielle Ausführung umgestellt werden soll, dass es Mails sind, spielt dabei keine große Rolle.
 
  • Gefällt mir
Reaktionen: CyborgBeta und nutrix
Kann den Titel nicht mehr ändern, aber ja, "+ parallele Ausführung in sequentielle Ausführung umwandeln" wäre naheliegender.

tollertyp schrieb:
Also dann ein ThreadPool Executor, der max. 1 Thread gleichzeitig ausführt? Imho, könnte dann immer noch zwischen allen eingereihten Tasks gewechselt werden. == parallel.
 
wer würde dann dazwischen Wechseln?
Ich würde das mit dem 1-Thread-ThreadPool auch nicht empfehlen.

Ich würde ja nach Feierabend mal selbst den Code probieren
 
  • Gefällt mir
Reaktionen: CyborgBeta
tollertyp schrieb:
Ich würde ja nach Feierabend mal selbst den Code probieren
Ok, mach das und ich gehe jetzt mal Mittagessen. :D Bis nachher.
 
Um Gottes Willen, was ist das denn? 😜
CyborgBeta schrieb:
Ich möchte den gerne de-synchronisieren, es soll also alles sequenziell, anstatt parallel ablaufen. Wie bekomme ich denn am besten die CyclicBarrier "weg"?
Warum, funktioniert irgendwas nicht? Falls ja:
Könnte dann vielleicht funktionieren.

Ansonsten, allgemeiner Tipp: eliminiere den globalen Status - ist zu fehleranfällig. Mach aus receive10() eine Methode des Accounts, die die Nachrichten zurückgibt, und aus deren lokalen Variablen Instance-Properties, so dass die nicht abhängig von state loopt, sondern einfach öfters aufgerufen wird. Und mach aus der Barrier-Action eine eigene Methode, der die Nachrichten übergeben werden und die zurückgibt, ob weitergemacht werden soll. Dann baue einen Loop drumherum. So hast Du auch viel bessere Möglichkeiten, das zu (re-)kombinieren.

Oder implementiere ein gescheites Producer-Consumer-Pattern.
 
  • Gefällt mir
Reaktionen: CyborgBeta
H4110 schrieb:
Warum, funktioniert irgendwas nicht?
Schreibe gerade vom Smartphone aus...

Doch, es funktioniert. Aber eben parallel... Das heißt, bei mehreren Accounts werden diese quasi gleichzeitig abgerufen.

Das Problem, was ich umgehen wollte, ist das Öffnen, geöffnet Lassen und hinterher erst das sichere Schließen aller IMAP-"Streams/-Verbindungen".

Sprich, ich möchte nicht für jede einzelne Mail extra eine Verbindung öffnen und schließen...

Aber ich sehe schon, dass ich um eine Account-Klasse mit Attributen und Zuständen vermutlich nicht drum herum komme, und damit die bisherige Codebasis "wegwerfen" kann...

Ideal wäre es, wenn ich mit einer einfachen boolean-Variable angeben könnte, ob die Verarbeitung parallel oder nicht ablaufen soll, aber das gibt mein Code bisher noch nicht her, Design-Fehlentscheidung also. ;)
 
Soll denn die Eingabe für alle Konten gelten?
System.out.println("a or a-b or empty or r or all:");
Und was bedetuet das überhaupt? :-)

Okay verstanden: "a" und "b" sind Nummern, r ist wohl fertig?

Aber was willst du denn konkret haben?
 
CyborgBeta schrieb:
Sprich, ich möchte nicht für jede einzelne Mail extra eine Verbindung öffnen und schließen...
an welcher Stelle passiert das denn deiner Meinung nach? Du oeffnest pro Account eine Verbindung und holst die Mails ab, oder nicht?

Im uebrigen, fuer dein Refactoring, Folder und Store implementieren das AutoClosable Interface
 
tollertyp schrieb:
System.out.println("a or a-b or empty or r or all:");
Und was bedetuet das überhaupt? :-)

Okay verstanden: "a" und "b" sind Nummern, r ist wohl fertig?
Bisschen unglücklich formuliert. ;)

Wenn ich "r" eingebe, soll die Liste mit allen E-Mails aktualisiert werden, bei "all" sollen alle E-Mails geöffnet werden, und bei einer Zahl (oder einem Zahlenbereich) soll die jeweilige E-Mail geöffnet werden.

tollertyp schrieb:
Soll denn die Eingabe für alle Konten gelten?
Ja.

abcddcba schrieb:
Im uebrigen, fuer dein Refactoring, Folder und Store implementieren das AutoClosable Interface
Jup, aber das wäre dann, nach der Einführung einer Account-Klasse, nutzlos.
 
CyborgBeta schrieb:
Ideal wäre es, wenn ich mit einer einfachen boolean-Variable angeben könnte, ob die Verarbeitung parallel oder nicht ablaufen soll,
Das ist einfach: nimm abhängig von der Boolean-Variable accounts.stream() oder accounts.parallelStream(), und arbeite dann mit dem.
CyborgBeta schrieb:
Jup, aber das wäre dann, nach der Einführung einer Account-Klasse, nutzlos.
Die kann dann einfach auch AutoCloseable implementieren und in ihrem close() emailStore.close() und emailFolder.close() aufrufen (natürlich jeweils mit Null-Check und try/catch drumherum oder mit IOUtils.closeQuietly()).
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: CyborgBeta
So, ich habe ein wenig weitergemacht... Von dem ursprünglichen Code ist aber nicht mehr viel übrig...

Es gibt nun eine Klasse AccountConnection, die die Verbindung verwaltet, und eine Klasse AccountManager, die die `AccountConnection`s verwaltet.

Nächste Schritte sind: Oben eine Angabe, wie viele E-Mails geladen werden sollen, eine Tabelle in einer Tabelle pro Account... (nicht alles mehr nur in einer Tabelle), E-Mails ggf. als "gelesen" markieren und gelesen E-Mails anders darstellen...

Diagramm:

1722684254224.png


Code:

Java:
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import jakarta.mail.*;
import jakarta.mail.internet.MimeMultipart;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.List;
import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellRenderer;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Safelist;

public class Main {
  record Account(
      int index, String host, int port, String user, String password, String inboxName) {}

  static class AccountConnection {
    private final Account account;
    private final Properties properties = new Properties();
    private Store emailStore;
    private Folder emailFolder;

    public AccountConnection(Account account) {
      this.account = account;
      properties.put("mail.store.protocol", "imap");
      properties.put("mail.imap.ssl.enable", true);
      properties.put("mail.imap.host", account.host());
      properties.put("mail.imap.port", account.port());
    }

    public List<Message> getMessages() throws Exception {
      if (emailStore == null || emailFolder == null) {
        emailStore = Session.getInstance(properties).getStore();
        emailStore.connect(account.user(), account.password());
        emailFolder = emailStore.getFolder(account.inboxName());
        emailFolder.open(Folder.READ_ONLY);
      }
      List<Message> l = new ArrayList<>();
      int c1 = emailFolder.getMessageCount();
      int c2 = 0;
      int max_mails = 6;
      for (int i = c1; i > 0 && c2 < max_mails; i--, c2++) {
        l.add(emailFolder.getMessage(i));
      }
      return l;
    }

    public void close() throws Exception {
      System.out.println("Closing connection to: " + account.host());
      if (emailFolder != null) {
        emailFolder.close();
      }
      if (emailStore != null) {
        emailStore.close();
      }
    }
  }

  static class AccountManager {
    private static final List<Account> accounts = getAccounts();

    private final List<AccountConnection> connections = new ArrayList<>();

    public AccountManager() {
      for (Account account : accounts) {
        connections.add(new AccountConnection(account));
      }
    }

    public List<Message> getMessages() throws Exception {
      List<Message> l = new ArrayList<>();
      for (AccountConnection ac : connections) {
        l.addAll(ac.getMessages());
      }
      return l;
    }

    public void close() throws Exception {
      for (AccountConnection ac : connections) {
        ac.close();
      }
    }

    private static List<Account> getAccounts() {
      String path = "accounts.conf";
      File file = new File(path);
      if (!file.exists()) {
        try (PrintWriter pw = new PrintWriter(file, StandardCharsets.UTF_8)) {
          // Adjust your accounts configuration here or in the accounts.conf file
          pw.println(
              """
              {
                 "accounts":[
                    {
                       "index":1,
                       "host":"foo",
                       "port":993,
                       "user":"foo",
                       "password":"foo",
                       "inboxName":"INBOX"
                    },
                    {
                       "index":2,
                       "host":"foo",
                       "port":993,
                       "user":"foo",
                       "password":"foo",
                       "inboxName":"INBOX"
                    }
                 ]
              }
              """);
        } catch (IOException e) {
          throw new RuntimeException(e);
        }
      }
      Config config = ConfigFactory.parseFile(file);
      List<Account> l = new ArrayList<>();
      for (Config c : config.getConfigList("accounts")) {
        l.add(
            new Account(
                c.getInt("index"),
                c.getString("host"),
                c.getInt("port"),
                c.getString("user"),
                c.getString("password"),
                c.getString("inboxName")));
      }
      return l;
    }
  }

  static class MultiLineTableModel extends AbstractTableModel {
    private final List<String[]> data = new ArrayList<>();

    public void addRow(String[] row) {
      data.add(row);
      fireTableDataChanged();
    }

    public void clearRows() {
      data.clear();
      fireTableDataChanged();
    }

    @Override
    public int getRowCount() {
      return data.size();
    }

    @Override
    public int getColumnCount() {
      return 1;
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
      return data.get(rowIndex);
    }

    @Override
    public String getColumnName(int column) {
      return "Messages";
    }
  }

  static class MultiLineTableCellRenderer extends JList<String> implements TableCellRenderer {
    @Override
    public Component getTableCellRendererComponent(
        JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
      if (value instanceof String[]) {
        setListData((String[]) value);
      }

      if (isSelected) {
        setBackground(UIManager.getColor("Table.selectionBackground"));
      } else {
        setBackground(UIManager.getColor("Table.background"));
      }

      return this;
    }
  }

  private static List<Message> messages = new ArrayList<>();

  private static String getDate(Message message) throws Exception {
    return message.getSentDate().toString();
  }

  private static String getFrom(Message message) throws Exception {
    StringBuilder sb = new StringBuilder();
    for (Address a : message.getFrom()) {
      sb.append(a.toString());
    }
    return sb.toString();
  }

  private static String getSubject(Message message) throws Exception {
    return message.getSubject();
  }

  private static String getContent(Message message) throws Exception {
    StringBuilder sb = new StringBuilder();
    if (message.isMimeType("text/plain")) {
      sb.append(message.getContent().toString());
    }
    if (message.isMimeType("multipart/*")) {
      MimeMultipart mimeMultipart = (MimeMultipart) message.getContent();
      for (int i = 0; i < mimeMultipart.getCount(); i++) {
        Document document = Jsoup.parse(mimeMultipart.getBodyPart(i).getContent().toString());
        document.outputSettings(new Document.OutputSettings().prettyPrint(false));
        document.select("br").append("\\n");
        document.select("p").prepend("\\n\\n");
        String s = document.html().replaceAll("\\\\n", "\n");
        sb.append(
            Jsoup.clean(s, "", Safelist.none(), new Document.OutputSettings().prettyPrint(false)));
      }
    }
    return sb.toString();
  }

  private static void createGUI() {
    JButton reloadButton = new JButton("Reload");
    JButton displayButton = new JButton("Display");

    MultiLineTableModel leftTableModel = new MultiLineTableModel();
    JTable leftTable = new JTable(leftTableModel);
    leftTable.getColumnModel().getColumn(0).setCellRenderer(new MultiLineTableCellRenderer());
    leftTable.setRowHeight((leftTable.getRowHeight() + 3) * 3);

    JTextArea rightTable = new JTextArea();
    rightTable.setLineWrap(true);
    rightTable.setWrapStyleWord(true);
    rightTable.setEditable(false);
    rightTable.setFont(new Font("Monospaced", Font.PLAIN, 12));

    JPanel bp1 = new JPanel(new FlowLayout(FlowLayout.LEFT));
    bp1.add(reloadButton);
    JPanel bp2 = new JPanel(new FlowLayout(FlowLayout.LEFT));
    bp2.add(displayButton);
    JPanel p1 = new JPanel(new GridLayout(1, 2));
    p1.add(bp1);
    p1.add(bp2);
    JPanel p2 = new JPanel(new GridLayout(1, 2));
    p2.add(new JScrollPane(leftTable));
    p2.add(new JScrollPane(rightTable));
    JFrame f = new JFrame("Projektname");
    f.setLayout(new BorderLayout());
    f.add(p1, BorderLayout.NORTH);
    f.add(p2, BorderLayout.CENTER);
    f.setSize(1200, 800);
    f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    f.setVisible(true);

    AccountManager am = new AccountManager();
    reloadButton.addActionListener(
        e -> {
          try {
            leftTableModel.clearRows();
            messages = am.getMessages();
            for (Message m : messages) {
              leftTableModel.addRow(new String[] {getDate(m), getFrom(m), getSubject(m)});
            }
          } catch (Exception ex) {
            throw new RuntimeException(ex);
          }
        });
    displayButton.addActionListener(
        e -> {
          try {
            int i = leftTable.getSelectedRow();
            if (i >= 0) {
              Message m = messages.get(i);
              rightTable.setText(
                  getDate(m)
                      + "\n\n"
                      + getFrom(m)
                      + "\n\n"
                      + getSubject(m)
                      + "\n\n"
                      + getContent(m));
              rightTable.setCaretPosition(0);
            }
          } catch (Exception ex) {
            throw new RuntimeException(ex);
          }
        });

    f.addWindowListener(
        new WindowAdapter() {
          @Override
          public void windowClosing(WindowEvent e) {
            try {
              am.close();
            } catch (Exception ex) {
              throw new RuntimeException(ex);
            }
          }
        });
  }

  public static void main(String[] args) {
    try {
      createGUI();
    } catch (Exception e) {
      JOptionPane.showMessageDialog(
          null,
          e.getMessage() + "\n\n" + Arrays.toString(e.getStackTrace()),
          "Error",
          JOptionPane.ERROR_MESSAGE);
    }
  }
}

Abhängigkeiten:

Code:
dependencies {
    // https://mvnrepository.com/artifact/com.sun.mail/jakarta.mail
    implementation 'com.sun.mail:jakarta.mail:2.0.1'
    // https://mvnrepository.com/artifact/org.jsoup/jsoup
    implementation 'org.jsoup:jsoup:1.18.1'
    // https://mvnrepository.com/artifact/com.typesafe/config
    implementation 'com.typesafe:config:1.4.3'
}

Nach SpotBugs ist alles ok, aber vielleicht habe ich dennoch etwas Grobes übersehen... Also, wer mal da drauf schauen mag, gerne.
 
Wie schon erwähnt, startet das UI nicht im Event-Dispatch-Thread. Zitat: "Programs that ignore this rule may function correctly most of the time, but are subject to unpredictable errors that are difficult to reproduce."

In der Dokumentation ist ein Beispiel, wie es richtig geht. Du musst eigentlich nur den Code in main() mit SwingUtilities.invokeLater() wrappen.

Ansonsten habe ich mir beim Überfliegen lediglich öfters vorgestellt, wie elegant der Code mit funktionalen Ansätzen (Streams) oder Kotlin aussehen könnte. 😜
 
  • Gefällt mir
Reaktionen: CyborgBeta
@H4110 Ja, es in invokeLater() zu wrappen, wäre kein Problem ... (entweder, einfach den createGUI()-Aufruf oder die komplette Methode)

Streams, Lambda, funktionale Programmierung ... dafür müssten schon mehrere Stellen geändert werden, eigentlich jede Schleife ... das kann ich auch machen. Aber ich bin zwiegespalten, ob die Lesbarkeit wirklich erhöht wird, wenn man das imperative/prozedurale Paradigma verlässt. Mein Chef will aber auch alles mit Streams haben, wo dies möglich ist.

Ich möchte jetzt keine Programmiersprachen-Paradigmen-Diskussion lostreten. ;) Java bietet beides an.
 
Ich will's auch niemandem aufzwingen, aber ein Beispiel kann ich mir nicht verkneifen. 😉

Java:
public List<Message> getMessages() throws Exception {
  List<Message> l = new ArrayList<>();
  for (AccountConnection ac : connections) {
    l.addAll(ac.getMessages());
  }
  return l;
}

Mit Streams:
Java:
 public List<Message> getMessages() throws Exception {
  return connections.parallelStream()
    .flatMap(ac -> ac.getMessages().stream())
    .collect(Collectors.toList());
 }

Ist dann sogar multi-threaded, was Du ja ursprünglich wolltest.
 
  • Gefällt mir
Reaktionen: CyborgBeta
Zurück
Oben