sabre-io / dav

sabre/dav is a CalDAV, CardDAV and WebDAV framework for PHP
http://sabre.io
BSD 3-Clause "New" or "Revised" License
1.54k stars 347 forks source link

Any tutorial / pre-built backend? #673

Closed arukado-sama closed 9 years ago

arukado-sama commented 9 years ago

Hello!

I just installed sabredav on my Symfony project and I'm still a beginner. I wonder how can I add an interface for caldav/carddav (view/modify cards and calendars) in my application using sabredav. Is there any tutorial to create such an interface? I tested baikal but since the last update it doesn't work for me (error 403). I would like to directly show/modify calendars and address books in my application or in a pre-built interface.

Here is my sabredav serveur:

<?php

use
    Sabre\DAV,
    Sabre\DAV\Auth,
    Sabre\CalDAV,
    Sabre\CardDAV,
    Sabre\DAVACL;

$pdo = new \PDO('mysql:dbname=groupware; host=localhost','root','haruhi');
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);

//Mapping PHP errors to exceptions
function exception_error_handler($errno, $errstr, $errfile, $errline ) {
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}
set_error_handler("exception_error_handler");

// The autoloader
require 'vendor/autoload.php';

// Backends
$authBackend = new DAV\Auth\Backend\PDO($pdo);
$principalBackend = new DAVACL\PrincipalBackend\PDO($pdo);
$calendarBackend = new CalDAV\Backend\PDO($pdo);
$cardBackend = new CardDAV\Backend\PDO($pdo);

// Directory tree
$tree = array(
    new DAVACL\PrincipalCollection($principalBackend),
    new CalDAV\CalendarRootNode($principalBackend, $calendarBackend),
    new CardDAV\AddressBookRoot($principalBackend, $cardBackend)
);  

// The object tree needs in turn to be passed to the server class
$server = new DAV\Server($tree);

// You are highly encouraged to set your WebDAV server base url. Without it,
// SabreDAV will guess, but the guess is not always correct. Putting the
// server on the root of the domain will improve compatibility.
$server->setBaseUri('/Symfony/groupwareserver.php');

// Authentication plugin
$authPlugin = new DAV\Auth\Plugin($authBackend,'groupware');
$server->addPlugin($authPlugin);

// CalDAV plugin
$caldavPlugin = new CalDAV\Plugin();
$server->addPlugin($caldavPlugin);

// CardDAV plugin
$carddavPlugin = new CardDAV\Plugin();
$server->addPlugin($carddavPlugin);

// ACL plugin
$aclPlugin = new DAVACL\Plugin();
$server->addPlugin($aclPlugin);

// Support for html frontend
$browser = new DAV\Browser\Plugin();
$server->addPlugin($browser);

//Scheduling
$server->addPlugin(new Sabre\CalDAV\Schedule\Plugin());

// And off we go!
$server->exec();

My server currently looks like this:

screenshot from 2015-06-03 14 05 03

Thanks in advance!

Hywan commented 9 years ago

Hello :-),

Unfortunately, there is no interface on-top of sabre/dav, this is just a server. What you're looking for is “clients”, to visualize address books, calendars, task lists and files.

If you feel brave, you can try sabre/katana, which is a ready-to-use sabre/dav server with an administration interface. You will find instructions inside this interface about how to connect a client easily.

arukado-sama commented 9 years ago

Thanks for your answer! I saw sabre-katana before, is there any possibility to edit calendars/cards without implementing a client from scratch? Like a bundle for Symfony? I'll try katana now.

evert commented 9 years ago

What's your use-case exactly? Could you just use an off-the-shelf client such as Thunderbird and Lightning? Or does it need to be integrated into a php web application?

arukado-sama commented 9 years ago

I'm working on a company manager called Groupware. I can use thunderbird for calendars/addressbooks, but some table views in my application require access to addressbook contacts etc.

Here is my application:

screenshot from 2015-06-03 15 30 57

For example the "Destinataire" column requires a company card to show the name of the associated company. The database is already built, it was used in the old Egroupware and my goal is too migrate the entire project on Symfony.

The old Egroupware:

screenshot from 2015-06-03 15 36 46

You can also see that there is a calendar link, optional if we can use thunderbird but it would be great to have a calendar interface like baikal2. In any case my company wants calendar sync.

evert commented 9 years ago

Hi @arukado-sama ,

So there's several different ways to interact with the data. I can't tell you which is the right one, and I don't have any tutorials.

  1. You can directly talk to the database, but this is highly discouraged.
  2. You can use the "backend classes" from your first code snippet you are instantiating several backends, such as Sabre\CalDAV\Backend\PDO. Those classes all have pretty basic apis that are relatively easy to understand. This is definitely an option for you.
  3. You can also make symfony speak 'caldav' and interact with the sabredav server using a HTTP client. This has the additional benefit that you create a logical separation between frontend and backend, and is also how we are building sabre/katana.

From a pure architectural point of view we believe that option 3 is superior, but going for option 2 allows you to get started quickly and see results pretty fast. Option 3 requires from you that you learn the protocols, and if you want to add any custom extensions, it could be a lot of extra work.

arukado-sama commented 9 years ago

@evert thanks again for helping me. How do I have to start with option 2? I mean, did I have to override some entities and controllers? Sadly I'm beginner at Symfony too, I implemented the current database into entities and used easy-admin bundle to show and edit them. I created some controller functions, twig views and javascripts but I don't know if I can understand how sabredav backends work. Do you think I should use sabre-katana to begin with?

evert commented 9 years ago

Alright then I misunderstood the question. You already have a database, and now you want to expose that data via sabre/dav.

In that case, look at Sabre\CalDAV\Backend\PDO and in particular Sabre\CalDAV\Backend\AbstractBackend. You need to extend that class and then use your symfony entities to return the correct data.

Closing this ticket, but feel free to ask follow-up questions in this ticket. Stackoverflow might also be useful.

arukado-sama commented 9 years ago

Thanks, I'll try this tomorrow and tell you if I encounter any problem!

arukado-sama commented 9 years ago

Hi!

Today I'm trying to use the company address book in Symfony. I got a vcf file which contains other companies addresses and put it in addressbooks/admin/addressbook/ on sabredav (a default address book I created). Don't know if it's the right thing to do. I extended AbstractBackend and PDO but really don't know what to do next. Can I also synchronize this address book on Thunderbird?

Thanks in advance!

EDIT: I found this tutorial http://sabre.io/dav/building-a-carddav-client/, but I don't know how to use such a language in Symfony --> Nevermind, I think it's for "option 3".

EDIT2: I tried something like this

<?php

namespace Groupware\AddressBookBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Groupware\AddressBookBundle\Entity\Backend\PDO;

class HomeController extends Controller
{
    public function indexAction()
    {
        $pdo = new PDO();

        $cards = $pdo->getCards($pdo->getAddressBooksForUser("principals/admin"));

        return $this->render('GroupwareAddressBookBundle:Home:index.html.twig', array('cards' => $cards));
    }
}

Without success because the PDO class requires a \PDO argument.

evert commented 9 years ago

Hey @arukado-sama ,

Without success because the PDO class requires a \PDO argument.

Give it the \PDO argument! What's stopping you?

arukado-sama commented 9 years ago

I don't really know what do I have to give. I tried with "new \PDO" but \PDO requires the same arguments so it will create an infinite loop. It's new for me I'm sorry I can't understand very well how it works.

evert commented 9 years ago

No, there's two classes:

Sabre\CardDAV\Backend\PDO, which requires a \PDO argument.

arukado-sama commented 9 years ago

This is what Eclipse tells me about the PDO class:

Groupware\AddressBookBundle\Entity\Backend\PDO::__construct(PDO $pdo, string $addressBooksTableName, string $cardsTableName, $addressBookChangesTableName)

I really don't know what I'm supposed to place on "PDO $pdo".

evert commented 9 years ago

new \PDO('...dsn to your database...')

arukado-sama commented 9 years ago

Wow, I didn't understand that. How can I retrieve the "dsn"?

evert commented 9 years ago

In your own example, it was:

$pdo = new \PDO('mysql:dbname=groupware; host=localhost','root','haruhi');
arukado-sama commented 9 years ago

I understand now, thanks! Do you think my vcf file is correctly placed on the server? I think this line is also wrong: $cards = $globalpdo->getCards($globalpdo->getAddressBooksForUser("principals/admin")); Because I got a conversion error. I'll search how to return the address book id.

evert commented 9 years ago

Yes it sounds correct.

arukado-sama commented 9 years ago

Hello!

Today I'm trying to retrieve cards with the address book id, here is my current function:

public function indexAction()
    {
        $pdo = new \PDO('mysql:dbname=groupware; host=localhost','root','haruhi');

        $globalpdo = new PDO($pdo);

        $addressbooks = $globalpdo->getAddressBooksForUser("principals/admin");

        $addressbook = $addressbooks[0];

        $cards = $globalpdo->getCards($addressbook->id);

        return $this->render('GroupwareAddressBookBundle:Home:index.html.twig', array('cards' => $cards));
    }

But it returns me an error at the moment because $addressbook isn't an object. I understood that getAddressBooksForUser() returns an array with all address books, or correct me if I'm wrong.

EDIT: I think I found a fix

$cards = $globalpdo->getCards($addressbook['id']);

I'll now try to show cardData for each card.

EDIT2: I got this error

Key "cardData" for array with keys "id, uri, lastmodified, etag, size" does not exist in GroupwareAddressBookBundle:Home:index.html.twig at line 19

When I try to show the uri of the card it works: addressbook.vcf But I can't show the data. In fact this card I retrieve isn't a Card object, it's an array, so I can't use functions from Card entity.

arukado-sama commented 9 years ago

Finally, I successfully retrieved card data:

public function indexAction()
    {
        $pdo = new \PDO('mysql:dbname=groupware; host=localhost','root','haruhi');

        $globalpdo = new PDO($pdo);

        $addressbooks = $globalpdo->getAddressBooksForUser("principals/admin");

        $addressbook = $addressbooks[0];

        $cards = $globalpdo->getCards($addressbook['id']);

        $card = new Card($globalpdo, $addressbook, $cards[0]);
        $data = $card->get();

        return $this->render('GroupwareAddressBookBundle:Home:index.html.twig', array('data' => $data));
    }

Unfortunately it shows the entire raw data, I need to retrieve each field of each card but don't know how to do it. Maybe by analyze the composition of the string, seems there is a function for that in the Card entity, I'll check out next week. Thanks for helping me about this, I was totally lost before that.

arukado-sama commented 9 years ago

Hello!

I would like to use vobject to parse,show and edit the vcf file (which contains multiple vcards) I talked about. I tried to use the tutorial (http://sabre.io/vobject/usage/) but I don't understand how to correctly parse my file. My first goal is to show all vcards in a table with a column for each property.

My vcard data is currently showing like this:

BEGIN:VCARD VERSION:2.1 ADR;WORK;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:;;Technoparc de l'Aubini=C3=A8re;NANTES;;44338;FRANCE CATEGORIES:Fournisseurs EMAIL;INTERNET;WORK:jbiarrotte@aplus-sa.com N:Biarrotte;Josette;;; FN:Josette Biarrotte ORG:A PLUS SYSTEME; TEL;FAX;WORK:01 69 88 97 97 TEL;WORK:02 51 13 51 79 UID:addressbook-792-e4ed915a41244c230071b9bd4a5c023d END:VCARD BEGIN:VCARD VERSION:2.1 ADR;WORK:;;38, rue de Panicale;LA VERRIERE;;78320; CATEGORIES:Clients N:LE BRAS;Syvie;;; FN:Syvie LE BRAS ORG:A2L; TEL;FAX;WORK:01 30 13 36 19 TEL;WORK:01 30 13 36 00 UID:addressbook-288-e4ed915a41244c230071b9bd4a5c023d END:VCARD BEGIN:VCARD VERSION:2.1 ADR;WORK;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:;;5, rue Caillardi=C3=A8re;BEAUCOUZE;;49070; CATEGORIES:Clients N:;;;; FN:A2POINTS ORG:A2POINTS; TEL;WORK:02 41 22 18 18 UID:addressbook-289-e4ed915a41244c230071b9bd4a5c023d END:VCARD BEGIN:VCARD VERSION:2.1 ADR;WORK:;;Rue de Docteur Weys;SAUMUR Cedex;;49428; CATEGORIES:Clients N:;;;; FN:AAS ORG:AAS; TEL;FAX;WORK:02 41 53 04 50 TEL;WORK:02 41 53 04 30 UID:addressbook-290-e4ed915a41244c230071b9bd4a5c023d END:VCARD BEGIN:VCARD VERSION:2.1 ADR;WORK:;;12 rue du champ de l'aire ZI des brunelleries;Bouchemaine;;49080;FRANCE CATEGORIES;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:Fournisseur M=C3=A9canique N:Plumelet;Estelle;;Mme; FN:Mme Estelle Plumelet ORG:AB CAOUTCHOUC; TEL;FAX;WORK:02 41 35 00 21 TEL;WORK:02 41 35 00 05 UID:addressbook-1240-ae182e169161504cd6086367f677b0e3 END:VCARD BEGIN:VCARD VERSION:2.1 ADR;WORK:;;C/ Travessera de les corts 346;BARCELONA;;08029;ESPAGNE CATEGORIES;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:Fournisseur M=C3=A9canique EMAIL;INTERNET;WORK:luis.abad@abbot-vault.com N:ABAD jose R;;;M.; FN:M. ABAD jose R ORG:ABBOT & VAULT; TEL;WORK;TYPE=PREF:34 93 444 82 21 UID:addressbook-291-e4ed915a41244c230071b9bd4a5c023d END:VCARD BEGIN:VCARD VERSION:2.1 ADR;WORK:;;ESPAGNE BARCELONA 08036;BARCELONA;;;ESPAGNE CATEGORIES:Fournisseurs N:;ABAD BARBARA;;ABBOT & VAULT; FN:ABBOT & VAULT ABAD BARBARA ORG:ABBOT&VAULT; TEL;FAX;WORK:34 93 217 03 50 TEL;WORK:34 93 237 18 40 UID:addressbook-773-e4ed915a41244c230071b9bd4a5c023d END:VCARD BEGIN:VCARD VERSION:2.1 ADR;WORK:;;BP 102;LOCHE;;37600;FRANCE CATEGORIES:Fournisseurs EMAIL;INTERNET;WORK:pascaldavignon@abe-transfo.com N:Davignon;Pascal;;M; FN:M Pascal Davignon ORG:ABE; TEL;CELL;WORK:06 75 02 22 43 TEL;FAX;WORK:02 47 92 10 23 TEL;WORK:02 47 92 10 22 UID:addressbook-1124-e4ed915a41244c230071b9bd4a5c023d END:VCARD 

etc.

Thanks in advance!

evert commented 9 years ago

You have to try better. What did you try, where did you faill?

Also, you should not ever have vCard version 2.1 files in the database, it will cause a lot of things to break. It needs to be 3.0 or 4.0.

arukado-sama commented 9 years ago

I'm currently importing all cards in entities. As we only have one address book for the company I think it would be more useful to have this address book implemented in card entities. Moreover I'm using the easy-admin bundle which can show entities table easier. I'll use sabre to export the data from the vcf file to create all entities the first time. Do you think it's a good idea? I already started to implement entities and a function which parse the data to create them.

evert commented 9 years ago

Hi!

Yes that sounds pretty reasonable to me.

arukado-sama commented 9 years ago

Hi!

Thanks to your documentation my calendar sync is now working with Thunderbird and sabredav. Unfortunately with the same method I can't sync Tunderbird with my address book, I think it's a bug but not sure. For now I successfully created entities for vcards on Symfony and I can export them to .vcf files. Now I'm trying to retrieve all the data from the current 2.1 vcf file (the one on the sabredav server) to create associated vcard entities, then export them into 4.0 vcards on the server (to synchronize Symfony entities with the sabredav address book, I guess I'll need a Symfony sync service). I don't really know how to correctly retrieve all information, I will have to analyze all lines. Here is my current test function (to see how the data is retrieved):

public function saveToDatabase()
    {
        $pdo = new \PDO('mysql:dbname=groupware; host=localhost','root','haruhi');

        $globalpdo = new PDO($pdo);

        $addressbooks = $globalpdo->getAddressBooksForUser("principals/admin");

        $addressbook = $addressbooks[0];

        $cards = $globalpdo->getCards($addressbook['id']);

        $card = new Card($globalpdo, $addressbook, $cards[0]);
        $data = $card->get();

        $i=0;
        while(strlen($data) > 20)
        //      while($i<10)
        {
            $vcard = VObject\Reader::read($data, VObject\Reader::OPTION_FORGIVING);

            $vcard->convert(VObject\Document::VCARD40);

            echo "name :".$vcard->N."<br/>";
            echo "name :".$vcard->FN."<br/>";
            echo "title :".$vcard->TITLE."<br/>";

            if(isset($vcard->TEL))
            {
                foreach($vcard->TEL as $tel)
                {
                    //print_r($tel, false);
                    echo 'Phone number: ', $tel, "<br/>";
                }
                echo 'Phone number: ', $vcard->TEL, "<br/>";
            }

            $data = substr($data, strpos($data, "END:VCARD") + 10);

            $i++;

        }

        return $card->get();
    }

Don't know if the 4.0 conversion is really needed here. I understood that there are different types of phone numbers, home, work, voice, etc. I'll try to parse the data (to eliminate semicolons), right now I don't know how to do it because I only did it in java with the "scanner" class. I'll search alone for now but if you have time and want to help me a little I'll be very grateful. Anyway thanks again for your help, I should be totally lost without you but now I can see the light at the end of the tunnel.

arukado-sama commented 9 years ago

Hello!

I successfully converted my huge 2.1 vcard in a 3.0 vcard, using a plugin on ThunderBird. I'm trying to retrieve parameters for each field of a vcard, without success. As you saw above I'm using $vcard->fieldname. I also tested with $vcard->__get("fieldname"). Unfortunately none of these commands can return parameters such as "WORK". For example I can retrieve all phone numbers with a foreach($vcard->TEL as $num) but without parameters I can't create vcard entities with it. Is it possible to retrieve parameters for each field? Is it also possible to parse address, name, etc?

Thanks in advance.

evert commented 9 years ago

http://sabre.io/vobject/usage/#working-with-parameters

arukado-sama commented 9 years ago

In fact I missed this documentation, I was looking in the CardDAV section. I'll try it today, thanks again!

arukado-sama commented 9 years ago

I'm encountering a problem when I try to retrieve parameters from telephone numbers. Here is my function to implement a VCard entity (in my database) retrieving sabredav address book:

public function saveToDatabase()
    {
        $pdo = new \PDO('mysql:dbname=groupware; host=localhost','root','haruhi');

        $globalpdo = new PDO($pdo);

        $addressbooks = $globalpdo->getAddressBooksForUser("principals/admin");

        $addressbook = $addressbooks[0];

        $cards = $globalpdo->getCards($addressbook['id']);

        $card = new Card($globalpdo, $addressbook, $cards[0]);
        $data = $card->get();

        $i=0;
        while($i<1)         // set to 1 to test with only 1 vcard from the remote address book
        {   

            $newcard = new VCard();
            $vcard = VObject\Reader::read($data, VObject\Reader::OPTION_FORGIVING);

            $newcard->setFirstName("test2");
            $newcard->setLastName("test2");

            if($vcard->CATEGORIES)
            {
                $em = $this->getDoctrine()->getManager();
                $categ = $em->getRepository('GroupwareAddressBookBundle:VCardCategorie');
                $CAT = str_replace(";", "", (string)$vcard->CATEGORIES);

                if(($CAT == "Clients")||($CAT == "Client"))
                    $newcard->setCat($categ->find(1));
                if(($CAT == "Fournisseurs")||($CAT == "Fournisseur"))
                    $newcard->setCat($categ->find(2));
                if(($CAT == "Clients/Fournisseurs")||($CAT == "Client / Fournisseur"))
                    $newcard->setCat($categ->find(3));
                if(($CAT == "Fournisseur Mécanique")||($CAT == "Fournisseur mécanique"))
                    $newcard->setCat($categ->find(4));
            }

            if($vcard->EMAIL)
            {
                $EMAIL = str_replace(";", "", (string)$vcard->EMAIL);
                $newcard->setMail($EMAIL);
            }

            if($vcard->ORG)
            {
                $ORG = str_replace(";", "", (string)$vcard->ORG);
                $newcard->setOrg($ORG);
            }

            if($vcard->URL)
            {
                $URL = str_replace(";", "", (string)$vcard->URL);
                $newcard->setUrl($URL);
            }

            if($vcard->NOTE)
            {
                $NOTE = str_replace(";", "", (string)$vcard->NOTE);
                $newcard->setUrl($NOTE);
            }

            $em = $this->getDoctrine()->getManager();
            $em->persist($newcard);
            $em->flush();

            if($vcard->TEL)
            {
                $ncard = $em->getRepository('GroupwareAddressBookBundle:VCard');

                foreach($vcard->TEL as $tel)
                {
                    $phone = new VCardTel();

                    $em = $this->getDoctrine()->getManager();
                    $type = $em->getRepository('GroupwareAddressBookBundle:VCardType');

                    $phone->setVCardId($ncard->find($newcard->getId()));

                    if($param = $tel['TYPE'])
                    {
                        foreach($param as $value)
                        {
                            if($value == "WORK") $phone->setType($type->find(1));
                            if($value == "HOME") $phone->setType($type->find(2));
                            if($value == "TEXT") $phone->setTypeText(true);
                            if($value == "VOICE") $phone->setTypeVoice(true);
                            if($value == "CELL") $phone->setTypeCell(true);
                            if($value == "FAX") $phone->setTypeFax(true);
                            if($value == "PAGER") $phone->setTypePager(true);
                            if($value == "VIDEO") $phone->setTypeVideo(true);
                            if($value == "TEXTPHONE") $phone->setTypeTextphone(true);
                        }
                    }

                    if(!$phone->getTypeText()) $phone->setTypeText(false);
                    if(!$phone->getTypeVoice()) $phone->setTypeVoice(false);
                    if(!$phone->getTypeCell()) $phone->setTypeCell(false);
                    if(!$phone->getTypeFax()) $phone->setTypeFax(false);
                    if(!$phone->getTypePager()) $phone->setTypePager(false);
                    if(!$phone->getTypeVideo()) $phone->setTypeVideo(false);
                    if(!$phone->getTypeTextphone()) $phone->setTypeTextphone(false);

                    $phone->setNumber((string)$tel);

                    $em->persist($phone);
                    $em->flush();
                }
            }

            $data = substr($data, strpos($data, "END:VCARD") + 10);

            $i++;

        }

        return $this->render('GroupwareAddressBookBundle:Home:index.html.twig');
    }

It works but when the telephone number has more than 1 parameter, for example "WORK, VOICE", only the first is saved in the database, as if my foreach didn't work. VCardTel is the telephone number entity which is associated to a VCard with VCardId.

Here is an example of this error, the VOICE parameter isn't added in my entity, and strangely the second number gets the WORK (Travail) attribute but it only has the FAX attribute on the card:

screenshot from 2015-06-18 17 10 28

screenshot from 2015-06-18 17 10 17

I tried to echo $value and this it what it returned: FAX WORK WORK

arukado-sama commented 9 years ago

Nevermind, I found the problem.

Here is the 2.1 vcard:

BEGIN:VCARD
VERSION:2.1
ADR;WORK;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:;;2 Avenue des Am=C3=A9thystes;NANTES;;44338;FRANCE
CATEGORIES:Fournisseurs
EMAIL;INTERNET;WORK:jbiarrotte@aplus-sa.com
N:Biarrotte;Josette;;;
FN:Josette Biarrotte
ORG:A PLUS SYSTEME;
TEL;FAX;WORK:01 69 88 97 97
TEL;WORK:02 51 13 51 79
END:VCARD

And here is the 3.0:

BEGIN:VCARD
VERSION:3.0
N;CHARSET=UTF-8:Biarrotte;Josette
FN;CHARSET=UTF-8:Josette Biarrotte
ORG;CHARSET=UTF-8:A PLUS SYSTEME;
ADR;TYPE=WORK,POSTAL;CHARSET=UTF-8:;;2 Avenue des Améthystes;NANTES;;44338;FRANCE
TEL;TYPE=WORK,VOICE:02 51 13 51 79
TEL;TYPE=FAX:01 69 88 97 97
EMAIL;TYPE=PREF,INTERNET:jbiarrotte@aplus-sa.com
CATEGORIES;CHARSET=UTF-8:Fournisseurs
END:VCARD

As you can see types are different, I have no idea what is causing this but I think the ThunderBird conversion isn't perfect at all. Problem resolved.